@bongos/core 1.21.32 → 1.21.34
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 +68 -43
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0338-modules-travel-through-a-bongos-hosted-store.md +7 -0
- package/docs/api/openapi.json +96 -3
- package/docs/api-reference.md +3 -2
- package/docs/architecture.md +1 -0
- package/docs/copy-inventory.md +29 -25
- package/docs/copy-registry.json +65 -29
- package/docs/design/craft-mode-direction.md +69 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +5 -1
- package/docs/page-readings.json +270 -201
- package/modules/government/catalog.js +3 -0
- package/modules/government/migrations/government_025_module_delist.sql +36 -0
- package/modules/hall-ui/public/modules.js +8 -1
- package/modules/hall-ui/public/street-desk.js +285 -0
- package/modules/hall-ui/public/street-inside.js +8 -4
- package/modules/hall-ui/public/street.css +59 -0
- package/modules/hall-ui/public/street.html +9 -3
- package/modules/hall-ui/public/street.js +219 -17
- package/modules/hall-ui/public/street.states.json +83 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/module.js +21 -5
- package/src/bongos/module-store.js +31 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/modules.js +35 -0
- package/src/module-api.js +1 -1
- package/tests/hall_catalog_world.mjs +1 -1
- package/tests/hall_matter_card.mjs +1 -1
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_desk.mjs +328 -0
- package/tests/hall_street_inside.mjs +1 -1
- package/tests/module_catalog.mjs +34 -6
- package/tests/module_store_delist.mjs +146 -0
|
@@ -41,6 +41,16 @@
|
|
|
41
41
|
// request, says the answer in a toast, re-reads the feeds so the pins, the
|
|
42
42
|
// readings and the evening follow, and marks the building that answered (a CSS
|
|
43
43
|
// class for 1.6 s: it rises or lights, and the matter's pin settles).
|
|
44
|
+
//
|
|
45
|
+
// THE FLOOR DESK (task 1004558). The chosen floor's block is the desk from the
|
|
46
|
+
// round 2 board (Desks.dc.html): GovDesk.deskModel() (street-desk.js) gives the
|
|
47
|
+
// header, tiles, rows, quiet state and record line under the line rule, and
|
|
48
|
+
// this file draws it. A row's buttons send the matter card's own request
|
|
49
|
+
// (GovMatterCard.request, the same routes), so acting happens in place. The
|
|
50
|
+
// game layer: the today meter in the street header (one pip per decision, kept
|
|
51
|
+
// for the day in this browser), a row that clears away (a CSS class, then the
|
|
52
|
+
// redraw), a cleared floor that lights its windows and shows a tick in the
|
|
53
|
+
// drawing, and the toast after every act.
|
|
44
54
|
|
|
45
55
|
(function () {
|
|
46
56
|
'use strict';
|
|
@@ -70,6 +80,11 @@
|
|
|
70
80
|
var data = { docket: null, goals: [], rollups: [], earnings: null, loaded: false, tasks: {}, room: null, constitution: null };
|
|
71
81
|
var card = { cv: null, form: null, error: '', busy: false, fresh: null, answer: null };
|
|
72
82
|
var view = { open: null, opener: null, model: null, scale: 1, mode: null, inside: null, insideModel: null };
|
|
83
|
+
// the desk's own state: rows you acted on this visit, what you did per floor,
|
|
84
|
+
// the floors you cleared, the row whose facts or chips are open
|
|
85
|
+
var desk = { done: {}, mine: {}, cleared: {}, open: null, ask: null, busy: false, roomAsked: false, model: null };
|
|
86
|
+
var TODAY_KEY = 'gov-desk-today';
|
|
87
|
+
var today = { day: '', n: 0 };
|
|
73
88
|
|
|
74
89
|
// ---------- light / dark ----------
|
|
75
90
|
var chosen = null;
|
|
@@ -343,6 +358,7 @@
|
|
|
343
358
|
var eveLine = $('streetEveningLine');
|
|
344
359
|
if (eveLine) eveLine.textContent = m.eveningLine;
|
|
345
360
|
|
|
361
|
+
drawToday();
|
|
346
362
|
$('hudNeeds').textContent = m.hud.needs;
|
|
347
363
|
$('hudMoving').textContent = m.hud.moving;
|
|
348
364
|
$('hudShare').textContent = m.hud.share;
|
|
@@ -392,7 +408,18 @@
|
|
|
392
408
|
|
|
393
409
|
function drawBuilding(mode) {
|
|
394
410
|
var st = view.inside;
|
|
395
|
-
var
|
|
411
|
+
var D = window.GovDesk;
|
|
412
|
+
// a building opens on the floor that needs you first (the desk's work)
|
|
413
|
+
var startFloor = st.floor == null && D && data.docket ? D.firstNeeds(st.key, data.docket) : null;
|
|
414
|
+
var cleared = [];
|
|
415
|
+
Object.keys(desk.cleared).forEach(function (k) { var p = k.split(':'); if (p[0] === st.key) cleared.push(Number(p[1])); });
|
|
416
|
+
var bv = I.buildingModel({ key: st.key, docket: data.docket, goals: data.goals, mode: mode, now: Date.now(), floor: st.floor == null ? startFloor : st.floor, drawer: st.drawer, picks: st.picks, cleared: cleared });
|
|
417
|
+
// a sitting's ballot is read once, so its row can vote in place
|
|
418
|
+
var C = window.GovMatterCard;
|
|
419
|
+
if (!desk.roomAsked && !data.room && C && data.docket && data.docket.matters.some(function (x) { return x && x.building === st.key && C.needsRoom(x); })) {
|
|
420
|
+
desk.roomAsked = true;
|
|
421
|
+
loadRoom().then(function () { if (view.inside) draw(); });
|
|
422
|
+
}
|
|
396
423
|
view.insideModel = bv;
|
|
397
424
|
setText('insideKicker', bv.kicker);
|
|
398
425
|
show('insidePanel', true); show('insideDHead', false); show('insideLegend', false);
|
|
@@ -481,7 +508,7 @@
|
|
|
481
508
|
// the chosen floor: two blocks that take turns, so the block re-enters on every pick
|
|
482
509
|
var on = $(bv.flipA ? 'insideOpenA' : 'insideOpenB'), off = $(bv.flipA ? 'insideOpenB' : 'insideOpenA');
|
|
483
510
|
if (off) { off.hidden = true; clear(off); }
|
|
484
|
-
if (on) { on.hidden = false;
|
|
511
|
+
if (on) { on.hidden = false; fillDesk(on, bv); }
|
|
485
512
|
setText('insideOnFile', bv.onFile);
|
|
486
513
|
var drawers = $('insideDrawers'); clear(drawers);
|
|
487
514
|
bv.drawers.forEach(function (dw) {
|
|
@@ -505,28 +532,201 @@
|
|
|
505
532
|
});
|
|
506
533
|
}
|
|
507
534
|
|
|
508
|
-
|
|
535
|
+
// ---------- the floor desk (task 1004558) ----------
|
|
536
|
+
function icon(d, f, size) {
|
|
537
|
+
var svg = svgEl('svg');
|
|
538
|
+
svg.setAttribute('width', String(size)); svg.setAttribute('height', String(size)); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true');
|
|
539
|
+
var a = svgEl('path'); a.setAttribute('d', d || '');
|
|
540
|
+
svg.appendChild(a);
|
|
541
|
+
if (f != null) { var b = svgEl('path'); b.setAttribute('d', f || ''); svg.appendChild(b); }
|
|
542
|
+
return svg;
|
|
543
|
+
}
|
|
544
|
+
function chipBtn(cls, text, onClick) {
|
|
545
|
+
var b = node('button', 'desk-chip ' + cls, text);
|
|
546
|
+
b.setAttribute('type', 'button');
|
|
547
|
+
if (desk.busy) b.setAttribute('disabled', '');
|
|
548
|
+
b.addEventListener('click', onClick);
|
|
549
|
+
return b;
|
|
550
|
+
}
|
|
551
|
+
function fillDesk(el, bv) {
|
|
509
552
|
clear(el);
|
|
553
|
+
var D = window.GovDesk, st = view.inside;
|
|
554
|
+
if (!D || !st) return;
|
|
555
|
+
var k = st.key + ':' + bv.selFloor;
|
|
556
|
+
var dm = D.deskModel({ key: st.key, floor: bv.selFloor, docket: data.docket, goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), now: Date.now(), room: data.room, constitution: data.constitution, done: desk.done, mine: desk.mine[k] || [], cleared: !!desk.cleared[k], today: today.n, sym: bv.sym });
|
|
557
|
+
desk.model = dm;
|
|
558
|
+
el.className = 'inside-open desk' + (dm.quiet ? ' desk--quiet' : '');
|
|
559
|
+
el.setAttribute('data-desk', dm.key);
|
|
560
|
+
// the header: the floor's sign, its name, its status
|
|
510
561
|
var head = node('div', 'inside-open__head');
|
|
511
562
|
var plate = node('span', 'inside-plate');
|
|
512
|
-
|
|
513
|
-
svg.setAttribute('width', '40'); svg.setAttribute('height', '40'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true');
|
|
514
|
-
var a = svgEl('path'); a.setAttribute('d', bv.sym.d || '');
|
|
515
|
-
var b = svgEl('path'); b.setAttribute('d', bv.sym.f || '');
|
|
516
|
-
svg.appendChild(a); svg.appendChild(b);
|
|
517
|
-
plate.appendChild(svg);
|
|
563
|
+
plate.appendChild(icon(bv.sym.d, bv.sym.f, 34));
|
|
518
564
|
head.appendChild(plate);
|
|
519
565
|
var words = node('div', 'inside-open__words');
|
|
520
|
-
words.appendChild(node('span', 'inside-open__kicker', bv.
|
|
521
|
-
words.appendChild(node('h3', 'inside-open__title',
|
|
566
|
+
words.appendChild(node('span', 'inside-open__kicker', 'floor ' + (bv.selFloor + 1)));
|
|
567
|
+
words.appendChild(node('h3', 'inside-open__title', dm.name));
|
|
568
|
+
words.appendChild(node('span', 'desk-status' + (dm.hot ? ' desk-status--hot' : ''), dm.status));
|
|
522
569
|
head.appendChild(words);
|
|
523
570
|
el.appendChild(head);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
571
|
+
// up to three tiles, none of them a zero
|
|
572
|
+
if (dm.tiles.length) {
|
|
573
|
+
var tiles = node('div', 'desk-tiles');
|
|
574
|
+
dm.tiles.forEach(function (t) {
|
|
575
|
+
var tl = node('div', 'desk-tile');
|
|
576
|
+
tl.appendChild(node('span', 'desk-tile__v', t.v));
|
|
577
|
+
tl.appendChild(node('span', 'desk-tile__l', t.l));
|
|
578
|
+
tiles.appendChild(tl);
|
|
579
|
+
});
|
|
580
|
+
el.appendChild(tiles);
|
|
581
|
+
}
|
|
582
|
+
if (dm.quiet) {
|
|
583
|
+
// one line, lit: never an empty list
|
|
584
|
+
var q = node('div', 'desk-quiet' + (dm.cleared ? ' desk-quiet--cleared' : ''));
|
|
585
|
+
var tick = icon('M7.5 12.2l3 3 6-6.5', null, 44);
|
|
586
|
+
tick.setAttribute('class', 'desk-quiet__tick');
|
|
587
|
+
var ring = svgEl('circle'); ring.setAttribute('cx', '12'); ring.setAttribute('cy', '12'); ring.setAttribute('r', '9.5');
|
|
588
|
+
tick.appendChild(ring);
|
|
589
|
+
q.appendChild(tick);
|
|
590
|
+
q.appendChild(node('span', 'desk-quiet__line', dm.clearLine));
|
|
591
|
+
q.appendChild(node('span', 'desk-quiet__sub', dm.clearSub));
|
|
592
|
+
el.appendChild(q);
|
|
593
|
+
} else {
|
|
594
|
+
var list = node('div', 'desk-rows');
|
|
595
|
+
dm.rows.forEach(function (r) { list.appendChild(deskRow(r)); });
|
|
596
|
+
el.appendChild(list);
|
|
597
|
+
}
|
|
598
|
+
// the record line: what you just did here, then what this building closed
|
|
599
|
+
var rec = node('div', 'desk-record');
|
|
600
|
+
rec.appendChild(node('span', 'desk-record__label', 'record'));
|
|
601
|
+
if (!dm.record.length) rec.appendChild(node('span', 'desk-record__item', 'nothing yet'));
|
|
602
|
+
dm.record.forEach(function (x, i) { rec.appendChild(node('span', 'desk-record__item' + (x.mine ? ' desk-record__item--mine' : '') + (x.mine && i === 0 ? ' desk-pop' : ''), x.text)); });
|
|
603
|
+
el.appendChild(rec);
|
|
604
|
+
}
|
|
605
|
+
function deskRow(r) {
|
|
606
|
+
var row = node('div', 'desk-row' + (r.needs ? ' desk-row--needs' : ''));
|
|
607
|
+
row.setAttribute('data-row', r.key);
|
|
608
|
+
var main = node('div', 'desk-row__main');
|
|
609
|
+
main.appendChild(node('span', 'desk-ini', r.ini));
|
|
610
|
+
var name = node('button', 'desk-name');
|
|
611
|
+
name.setAttribute('type', 'button');
|
|
612
|
+
name.setAttribute('aria-expanded', desk.open === r.key ? 'true' : 'false');
|
|
613
|
+
name.setAttribute('data-matter', r.matter);
|
|
614
|
+
name.appendChild(node('span', 'desk-name__t', r.name));
|
|
615
|
+
name.appendChild(node('span', 'desk-name__f', r.fact));
|
|
616
|
+
name.addEventListener('click', function () { desk.open = desk.open === r.key ? null : r.key; desk.ask = null; draw(); focusRow(r.key, 'name'); });
|
|
617
|
+
main.appendChild(name);
|
|
618
|
+
var acts = node('span', 'desk-acts');
|
|
619
|
+
if (r.wait) acts.appendChild(node('span', 'desk-wait', r.wait));
|
|
620
|
+
// the primary button sits rightmost, as on the board
|
|
621
|
+
r.actions.slice().reverse().forEach(function (a) {
|
|
622
|
+
acts.appendChild(chipBtn('desk-chip--' + a.tone, a.label, function () {
|
|
623
|
+
if (a.go) { window.location.href = a.go; return; }
|
|
624
|
+
if (a.ask) { desk.ask = desk.ask === r.key ? null : r.key; desk.open = null; draw(); focusRow(r.key, 'chip'); return; }
|
|
625
|
+
deskAct(r, a.key, {});
|
|
626
|
+
}));
|
|
529
627
|
});
|
|
628
|
+
main.appendChild(acts);
|
|
629
|
+
row.appendChild(main);
|
|
630
|
+
if (desk.open === r.key) {
|
|
631
|
+
var facts = node('div', 'desk-facts');
|
|
632
|
+
r.facts.forEach(function (f) { facts.appendChild(node('span', 'desk-fact', f)); });
|
|
633
|
+
row.appendChild(facts);
|
|
634
|
+
}
|
|
635
|
+
if (desk.ask === r.key && r.chips) {
|
|
636
|
+
var chips = node('div', 'desk-choices');
|
|
637
|
+
chips.appendChild(node('span', 'desk-choices__ask', r.chips.ask));
|
|
638
|
+
r.chips.options.forEach(function (c) { chips.appendChild(chipBtn('desk-chip--choice', c.label, function () { deskAct(r, r.chips.for, c.input); })); });
|
|
639
|
+
row.appendChild(chips);
|
|
640
|
+
}
|
|
641
|
+
return row;
|
|
642
|
+
}
|
|
643
|
+
function deskRowsEl() {
|
|
644
|
+
var st = view.insideModel;
|
|
645
|
+
var on = $(st && st.flipA ? 'insideOpenA' : 'insideOpenB');
|
|
646
|
+
var kids = on ? on.children : [];
|
|
647
|
+
for (var i = 0; i < kids.length; i++) if (/\bdesk-rows\b/.test(kids[i].className)) return kids[i];
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
function focusRow(key, what) {
|
|
651
|
+
var list = deskRowsEl();
|
|
652
|
+
var rows = list ? list.children : [];
|
|
653
|
+
for (var i = 0; i < rows.length; i++) {
|
|
654
|
+
if (rows[i].getAttribute('data-row') !== key) continue;
|
|
655
|
+
var main = rows[i].children[0];
|
|
656
|
+
var target = what === 'chip' && rows[i].children[1] ? rows[i].children[1].children[1] : main && main.children[1];
|
|
657
|
+
if (target && target.focus) target.focus();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// The row clears away (a class the stylesheet animates), then the desk redraws without it.
|
|
662
|
+
function clearRow(key, then) {
|
|
663
|
+
var list = deskRowsEl();
|
|
664
|
+
var rows = list ? list.children : [];
|
|
665
|
+
for (var i = 0; i < rows.length; i++) if (rows[i].getAttribute('data-row') === key) rows[i].className += ' desk-row--gone';
|
|
666
|
+
if (typeof setTimeout !== 'function') return then();
|
|
667
|
+
setTimeout(then, 320);
|
|
668
|
+
}
|
|
669
|
+
var DONE_WORDS = { for: 'voted yes', object: 'voted no', fine: 'let in', refuse: 'turned away', release: 'released', hold: 'held' };
|
|
670
|
+
// One act from a row: checked, sent through the card's own request (the
|
|
671
|
+
// server's permission checks decide), then the row clears, the floor counts
|
|
672
|
+
// it, the meter takes a pip and the toast says what happened. An act that
|
|
673
|
+
// records nothing (hold) clears the row for this visit and says it still waits.
|
|
674
|
+
function deskAct(r, key, input) {
|
|
675
|
+
var C = window.GovMatterCard, st = view.inside;
|
|
676
|
+
if (!C || !st || !r.card || desk.busy) return Promise.resolve(false);
|
|
677
|
+
var refused = C.check(r.card, key, input);
|
|
678
|
+
if (refused) { say(refused); return Promise.resolve(false); }
|
|
679
|
+
var k = st.key + ':' + (view.insideModel ? view.insideModel.selFloor : 0);
|
|
680
|
+
var req = C.request(r.card, key, input);
|
|
681
|
+
var settle = function (body) {
|
|
682
|
+
var a = C.answer(r.card, key, body);
|
|
683
|
+
desk.mine[k] = [DONE_WORDS[key] + ' ' + r.name].concat(desk.mine[k] || []);
|
|
684
|
+
desk.done[r.matter] = true;
|
|
685
|
+
desk.open = null; desk.ask = null;
|
|
686
|
+
if (desk.model && !desk.model.rows.some(function (x) { return x.matter !== r.matter; })) desk.cleared[k] = true;
|
|
687
|
+
if (st.floor == null && view.insideModel) st.floor = view.insideModel.selFloor;
|
|
688
|
+
bumpToday();
|
|
689
|
+
if (a.building && a.kind) card.answer = { building: a.building, matter: r.matter, kind: a.kind, at: Date.now() };
|
|
690
|
+
say(a.toast || 'Done.');
|
|
691
|
+
};
|
|
692
|
+
if (!req) return new Promise(function (res) { clearRow(r.key, function () { settle({}); draw(); res(true); }); });
|
|
693
|
+
desk.busy = true;
|
|
694
|
+
draw();
|
|
695
|
+
return send(req).then(function (out) {
|
|
696
|
+
desk.busy = false;
|
|
697
|
+
if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
|
|
698
|
+
return new Promise(function (res) {
|
|
699
|
+
clearRow(r.key, function () {
|
|
700
|
+
settle(out.body);
|
|
701
|
+
draw();
|
|
702
|
+
data.room = null; desk.roomAsked = false;
|
|
703
|
+
load().then(function () { res(true); });
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
// the today meter: decisions made today, kept for the day in this browser
|
|
709
|
+
function dayOf() { return new Date().toISOString().slice(0, 10); }
|
|
710
|
+
function readToday() {
|
|
711
|
+
var t = { day: dayOf(), n: 0 };
|
|
712
|
+
try {
|
|
713
|
+
var raw = window.localStorage ? JSON.parse(window.localStorage.getItem(TODAY_KEY) || 'null') : null;
|
|
714
|
+
if (raw && raw.day === t.day && isFinite(raw.n)) t.n = Math.max(0, Math.floor(raw.n));
|
|
715
|
+
} catch (e) { /* storage blocked: the meter starts at zero */ }
|
|
716
|
+
return t;
|
|
717
|
+
}
|
|
718
|
+
function bumpToday() {
|
|
719
|
+
if (today.day !== dayOf()) today = { day: dayOf(), n: 0 };
|
|
720
|
+
today.n += 1;
|
|
721
|
+
try { if (window.localStorage) window.localStorage.setItem(TODAY_KEY, JSON.stringify(today)); } catch (e) { /* the count still shows for this visit */ }
|
|
722
|
+
}
|
|
723
|
+
function drawToday() {
|
|
724
|
+
var D = window.GovDesk, pipsEl = $('streetTodayPips');
|
|
725
|
+
if (!D || !pipsEl) return;
|
|
726
|
+
var t = D.todayPips(today.n);
|
|
727
|
+
clear(pipsEl);
|
|
728
|
+
t.pips.forEach(function (p) { pipsEl.appendChild(node('span', 'street-today__pip' + (p.on ? ' street-today__pip--on' : '') + (p.fresh ? ' desk-pop' : ''))); });
|
|
729
|
+
setText('streetTodayN', t.text);
|
|
530
730
|
}
|
|
531
731
|
|
|
532
732
|
function pickFloor(i) {
|
|
@@ -534,6 +734,7 @@
|
|
|
534
734
|
if (!st) return;
|
|
535
735
|
st.floor = i;
|
|
536
736
|
st.picks = (st.picks || 0) + 1;
|
|
737
|
+
desk.open = null; desk.ask = null;
|
|
537
738
|
draw();
|
|
538
739
|
var floors = $('insideFloors');
|
|
539
740
|
var kids = (floors && floors.children) || [];
|
|
@@ -929,11 +1130,12 @@
|
|
|
929
1130
|
if (win) win.addEventListener('scroll', placeCard);
|
|
930
1131
|
if (media && media.addEventListener) media.addEventListener('change', function () { if (!storedTheme()) { applyTheme(); draw(); } });
|
|
931
1132
|
painted = applyTheme();
|
|
1133
|
+
today = readToday();
|
|
932
1134
|
draw();
|
|
933
1135
|
watchShellTheme();
|
|
934
1136
|
return load();
|
|
935
1137
|
}
|
|
936
1138
|
|
|
937
|
-
window.GovStreetPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, actOn: actOn, pickFloor: pickFloor, goStreet: goStreet, amend: amend, fileDraft: fileDraft, FEEDS: FEEDS, THEME_KEY: THEME_KEY };
|
|
1139
|
+
window.GovStreetPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, actOn: actOn, pickFloor: pickFloor, deskAct: deskAct, goStreet: goStreet, amend: amend, fileDraft: fileDraft, FEEDS: FEEDS, THEME_KEY: THEME_KEY };
|
|
938
1140
|
window.GovStreetPage.ready = boot();
|
|
939
1141
|
}());
|
|
@@ -373,6 +373,89 @@
|
|
|
373
373
|
"#insideLegend"
|
|
374
374
|
]
|
|
375
375
|
}
|
|
376
|
+
},
|
|
377
|
+
"desk": {
|
|
378
|
+
"auth": true,
|
|
379
|
+
"_": "The floor desk (task 1004558): inside People the building opens on the floor that needs you, and the desk shows its sign, name and orange status, the tiles, the to-do rows with their two buttons, and the record line; the street header carries the today meter.",
|
|
380
|
+
"url": "/builders/street#building=people",
|
|
381
|
+
"actions": [
|
|
382
|
+
[
|
|
383
|
+
"wait",
|
|
384
|
+
900
|
|
385
|
+
],
|
|
386
|
+
[
|
|
387
|
+
"clickIfVisible",
|
|
388
|
+
"[data-craft-close]"
|
|
389
|
+
],
|
|
390
|
+
[
|
|
391
|
+
"wait",
|
|
392
|
+
300
|
|
393
|
+
]
|
|
394
|
+
],
|
|
395
|
+
"expect": {
|
|
396
|
+
"visible": [
|
|
397
|
+
".desk-rows",
|
|
398
|
+
".desk-status--hot",
|
|
399
|
+
".desk-record",
|
|
400
|
+
"#streetTodayPips"
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
"desk-facts": {
|
|
405
|
+
"auth": true,
|
|
406
|
+
"_": "A row name tapped: at most three facts open in place under it.",
|
|
407
|
+
"url": "/builders/street#building=people",
|
|
408
|
+
"actions": [
|
|
409
|
+
[
|
|
410
|
+
"wait",
|
|
411
|
+
900
|
|
412
|
+
],
|
|
413
|
+
[
|
|
414
|
+
"clickIfVisible",
|
|
415
|
+
"[data-craft-close]"
|
|
416
|
+
],
|
|
417
|
+
[
|
|
418
|
+
"wait",
|
|
419
|
+
300
|
|
420
|
+
],
|
|
421
|
+
[
|
|
422
|
+
"click",
|
|
423
|
+
".desk-name"
|
|
424
|
+
],
|
|
425
|
+
[
|
|
426
|
+
"wait",
|
|
427
|
+
500
|
|
428
|
+
]
|
|
429
|
+
],
|
|
430
|
+
"expect": {
|
|
431
|
+
"visible": [
|
|
432
|
+
".desk-facts"
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
"desk-quiet": {
|
|
437
|
+
"auth": true,
|
|
438
|
+
"_": "A quiet floor: one line with the lights on, never an empty list.",
|
|
439
|
+
"url": "/builders/street#building=review",
|
|
440
|
+
"actions": [
|
|
441
|
+
[
|
|
442
|
+
"wait",
|
|
443
|
+
900
|
|
444
|
+
],
|
|
445
|
+
[
|
|
446
|
+
"clickIfVisible",
|
|
447
|
+
"[data-craft-close]"
|
|
448
|
+
],
|
|
449
|
+
[
|
|
450
|
+
"wait",
|
|
451
|
+
300
|
|
452
|
+
]
|
|
453
|
+
],
|
|
454
|
+
"expect": {
|
|
455
|
+
"visible": [
|
|
456
|
+
".desk-quiet"
|
|
457
|
+
]
|
|
458
|
+
}
|
|
376
459
|
}
|
|
377
460
|
},
|
|
378
461
|
"reducedMotion": {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.34",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.34",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.34",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8948,5 +8948,21 @@
|
|
|
8948
8948
|
"id": "1004551",
|
|
8949
8949
|
"text": "An Archon can star a version on the Roadmap as the project's focus; it persists across reloads, and everyone else sees the star read-only (the server refuses non-Archons)."
|
|
8950
8950
|
}
|
|
8951
|
+
],
|
|
8952
|
+
"1.21.33": [
|
|
8953
|
+
{
|
|
8954
|
+
"id": "1003797",
|
|
8955
|
+
"text": "Senior builders can now take a module out of the store, with a reason. Nobody can newly install or update it, but anyone who already has it keeps it working and is told no more updates will come."
|
|
8956
|
+
},
|
|
8957
|
+
{
|
|
8958
|
+
"id": "1004494",
|
|
8959
|
+
"text": "The look for each craft mode is decided. Artistry, Ideating and Governing each get their own colour (moonlight, star gold, city blue) on the mode switch, the tab you are on and the top bar, the mode's name beside the page titl"
|
|
8960
|
+
}
|
|
8961
|
+
],
|
|
8962
|
+
"1.21.34": [
|
|
8963
|
+
{
|
|
8964
|
+
"id": "1004558",
|
|
8965
|
+
"text": "Inside every building in the governor city, the floor you pick is now a desk you can act from, built from the new floor desks design. It shows the floor's name and how many things wait on you, up to three short numbers, and on"
|
|
8966
|
+
}
|
|
8951
8967
|
]
|
|
8952
8968
|
}
|
package/scripts/gds/module.js
CHANGED
|
@@ -433,7 +433,7 @@ function clip(s, n) {
|
|
|
433
433
|
|
|
434
434
|
// Pure renderer — every input arrives as a parameter (the tests pass catalog
|
|
435
435
|
// rows straight in), so it touches no disk and no config.
|
|
436
|
-
function formatCatalog(rows, { catalogOnly = false, query = '', core = null } = {}) {
|
|
436
|
+
function formatCatalog(rows, { catalogOnly = false, query = '', core = null, delisted = new Set() } = {}) {
|
|
437
437
|
const scoped = catalogOnly ? catalog.catalogRows(rows) : rows;
|
|
438
438
|
const shown = catalog.searchCatalog(scoped, query);
|
|
439
439
|
const counts = catalog.summarize(rows);
|
|
@@ -458,11 +458,14 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null } =
|
|
|
458
458
|
// An inferred credit is marked so it never reads as something the author
|
|
459
459
|
// typed — the catalog's whole job is honest attribution (ADR 0107 §2).
|
|
460
460
|
const suffix = r.credit.inferred ? '*' : '';
|
|
461
|
-
const upkeep = (r.maintenance && r.maintenance.status) || 'undeclared';
|
|
461
|
+
const upkeep = ((r.maintenance && r.maintenance.status) || 'undeclared') + (delisted.has(r.key) ? ' · DELISTED' : '');
|
|
462
462
|
out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(clip(r.version || '—', 8), 9)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
|
|
463
463
|
}
|
|
464
464
|
out.push('');
|
|
465
465
|
out.push(' [x] enabled here · [ ] available, not enabled');
|
|
466
|
+
if (shown.some((r) => delisted.has(r.key))) {
|
|
467
|
+
out.push(' DELISTED = taken out of the store: your copy keeps working, but no more versions will come.');
|
|
468
|
+
}
|
|
466
469
|
if (shown.some((r) => r.credit.inferred)) {
|
|
467
470
|
out.push(` * credit not declared in module.json — shown as the platform (${catalog.CORE_PROJECT_NAME}), which ships it.`);
|
|
468
471
|
}
|
|
@@ -470,7 +473,18 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null } =
|
|
|
470
473
|
return out.join('\n');
|
|
471
474
|
}
|
|
472
475
|
|
|
473
|
-
|
|
476
|
+
// Which installed store modules the store has delisted (task 1003797). Only modules
|
|
477
|
+
// built into this instance's tree can be store installs, so a core checkout asks
|
|
478
|
+
// nothing. Fail-soft like the upgrade check: offline, or no session, lists as before.
|
|
479
|
+
async function gatherDelisted(rows, { checkStore = gatherUpdates } = {}) {
|
|
480
|
+
const held = rows.filter((r) => r.source === 'instance');
|
|
481
|
+
if (!held.length) return new Set();
|
|
482
|
+
const found = await checkStore(held);
|
|
483
|
+
if (!found.checked) return new Set();
|
|
484
|
+
return new Set(Object.keys(found.latest).filter((k) => found.latest[k] && found.latest[k].status === 'delisted'));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function cmdList(args, { log = console.log, errlog = console.error, gather = gatherCatalog, checkStore = gatherUpdates } = {}) {
|
|
474
488
|
const catalogOnly = args.includes('--catalog');
|
|
475
489
|
const json = args.includes('--json');
|
|
476
490
|
const sIdx = args.indexOf('--search');
|
|
@@ -484,14 +498,16 @@ function cmdList(args, { log = console.log, errlog = console.error, gather = gat
|
|
|
484
498
|
try { data = gather(); }
|
|
485
499
|
catch (e) { errlog(`bongos module list: ${e.message}`); return 1; }
|
|
486
500
|
|
|
501
|
+
const delisted = await gatherDelisted(data.rows, { checkStore });
|
|
502
|
+
|
|
487
503
|
if (json) {
|
|
488
504
|
const scoped = catalogOnly ? catalog.catalogRows(data.rows) : data.rows;
|
|
489
|
-
const modules = catalog.searchCatalog(scoped, query);
|
|
505
|
+
const modules = catalog.searchCatalog(scoped, query).map((m) => ({ ...m, store_delisted: delisted.has(m.key) }));
|
|
490
506
|
log(JSON.stringify({ core: data.core, summary: catalog.summarize(data.rows), modules }, null, 2));
|
|
491
507
|
return 0;
|
|
492
508
|
}
|
|
493
509
|
|
|
494
|
-
log(formatCatalog(data.rows, { catalogOnly, query, core: data.core }));
|
|
510
|
+
log(formatCatalog(data.rows, { catalogOnly, query, core: data.core, delisted }));
|
|
495
511
|
for (const s of data.skipped) {
|
|
496
512
|
errlog(` ! ${s.key}: not listed — ${s.reason}`);
|
|
497
513
|
}
|
|
@@ -232,6 +232,36 @@ async function latestVersions(keys, { db } = {}) {
|
|
|
232
232
|
return out;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// Delist a module (task 1003797, ADR 0338 D2): it leaves the catalog, takes no new
|
|
236
|
+
// acquisitions or versions, and serves no further downloads — every read above
|
|
237
|
+
// already refuses on status 'delisted', so this one UPDATE is the whole act. It
|
|
238
|
+
// touches nothing else: no entitlement is revoked, no installed copy is reached,
|
|
239
|
+
// and no score row is written (a delist is not a score, ADR 0359). One-way here;
|
|
240
|
+
// there is no relist route yet.
|
|
241
|
+
// Returns { ok: true, module } or { ok: false, status, code, message }.
|
|
242
|
+
async function delistModule(key, { db } = {}) {
|
|
243
|
+
const pool = db || require('./pool').pool;
|
|
244
|
+
const { rows: [mod] } = await pool.query(
|
|
245
|
+
`UPDATE store_modules SET status = 'delisted', delisted_at = now()
|
|
246
|
+
WHERE module_key = $1 AND status = 'listed'
|
|
247
|
+
RETURNING module_key, title, status, delisted_at`, [key]);
|
|
248
|
+
if (mod) return { ok: true, module: mod };
|
|
249
|
+
const { rows: [have] } = await pool.query(
|
|
250
|
+
'SELECT module_key, status, delisted_at FROM store_modules WHERE module_key = $1', [key]);
|
|
251
|
+
if (!have) return { ok: false, status: 404, code: 'module_not_found', message: `The store has no module "${key}".` };
|
|
252
|
+
return { ok: false, status: 409, code: 'already_delisted', message: `"${key}" is already delisted.` };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Which of these module keys the store has delisted, as a Set — so the hall's
|
|
256
|
+
// Modules tab can mark an installed store module that will get no more versions.
|
|
257
|
+
async function delistedKeySet(keys, { db } = {}) {
|
|
258
|
+
if (!keys.length) return new Set();
|
|
259
|
+
const pool = db || require('./pool').pool;
|
|
260
|
+
const { rows } = await pool.query(
|
|
261
|
+
"SELECT module_key FROM store_modules WHERE module_key = ANY($1) AND status = 'delisted'", [keys]);
|
|
262
|
+
return new Set(rows.map((r) => r.module_key));
|
|
263
|
+
}
|
|
264
|
+
|
|
235
265
|
// The tarball on disk for a version row. artifact_path is relative to the store dir
|
|
236
266
|
// (relativeArtifactPath), and artifactPath() re-validates key + version, so a row
|
|
237
267
|
// can never point a read outside the store.
|
|
@@ -246,4 +276,5 @@ function versionArtifactFile(row, { dir = storeDir() } = {}) {
|
|
|
246
276
|
module.exports = {
|
|
247
277
|
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
248
278
|
isFreePrice, resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
|
|
279
|
+
delistModule, delistedKeySet,
|
|
249
280
|
};
|
|
@@ -240,6 +240,9 @@ const EXPECTED_RANKS = {
|
|
|
240
240
|
// Re-running a store version's assessment (task 1003796, ADR 0359) spends a Docs
|
|
241
241
|
// grader call and re-opens the Security gate, so it is pinned to its own atom.
|
|
242
242
|
'POST /store/modules/:key/versions/:version/reassess': 'perm:module.assessment.rerun',
|
|
243
|
+
// Delisting a store module (task 1003797, ADR 0338 D2) stops every new install of
|
|
244
|
+
// it for every instance, so it is pinned to its own atom.
|
|
245
|
+
'POST /store/modules/:key/delist': 'perm:module.delist',
|
|
243
246
|
};
|
|
244
247
|
|
|
245
248
|
// The authz-core route pins (ADR 0174). Paths and the gating atom were renamed
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
// GET /store/modules/:key/versions/:version/howto
|
|
36
36
|
// the HOWTO.md that version shipped with, as text for
|
|
37
37
|
// the hall's how-to page (task 1004366, ADR 0347).
|
|
38
|
+
// POST /store/modules/:key/delist
|
|
39
|
+
// Metic+ takes a module out of the store; holders
|
|
40
|
+
// keep it (task 1003797, ADR 0338 D2).
|
|
38
41
|
//
|
|
39
42
|
// Read is open to any signed-in builder (same gate as the atlas/primer pages);
|
|
40
43
|
// every write (enable/disable, submit) is metic+archon, same rank as
|
|
@@ -85,6 +88,7 @@ const { checkEntitlement, listEntitlements, grantEntitlement, recordAcquired } =
|
|
|
85
88
|
const {
|
|
86
89
|
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
87
90
|
resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
|
|
91
|
+
delistModule, delistedKeySet,
|
|
88
92
|
} = require('../module-store');
|
|
89
93
|
const { KEY_RE } = require('../../module-loader/manifest-schema');
|
|
90
94
|
const { parseVersion } = require('../../module-loader/semver');
|
|
@@ -204,6 +208,11 @@ module.exports = function buildModulesRouter() {
|
|
|
204
208
|
let published = new Set();
|
|
205
209
|
try { published = await publishedVersionSet(all.map((m) => m.key)); }
|
|
206
210
|
catch (e) { log.warn({ err: e.message }, 'store versions unreadable for the modules list'); }
|
|
211
|
+
// Which installed store modules the store has delisted (task 1003797), so the row
|
|
212
|
+
// says it keeps working but gets no more versions (ADR 0338 D2). Same tolerance.
|
|
213
|
+
let delistedKeys = new Set();
|
|
214
|
+
try { delistedKeys = await delistedKeySet(all.filter((m) => m.source === 'instance').map((m) => m.key)); }
|
|
215
|
+
catch (e) { log.warn({ err: e.message }, 'store status unreadable for the modules list'); }
|
|
207
216
|
for (const m of all) {
|
|
208
217
|
m.enabled_source = sources[m.key] || 'default';
|
|
209
218
|
m.override = byKey.has(m.key)
|
|
@@ -214,6 +223,7 @@ module.exports = function buildModulesRouter() {
|
|
|
214
223
|
m.store_howto = m.source === 'instance' && m.version && published.has(`${m.key}@${m.version}`)
|
|
215
224
|
? `/builders/modules?howto=${encodeURIComponent(m.key)}&version=${encodeURIComponent(String(m.version))}`
|
|
216
225
|
: null;
|
|
226
|
+
m.store_delisted = m.source === 'instance' && delistedKeys.has(m.key);
|
|
217
227
|
}
|
|
218
228
|
res.json({
|
|
219
229
|
modules: all,
|
|
@@ -446,6 +456,31 @@ module.exports = function buildModulesRouter() {
|
|
|
446
456
|
res.status(202).json({ ok: true, queued: true, module_key: key, version, version_id: r.version.id });
|
|
447
457
|
}));
|
|
448
458
|
|
|
459
|
+
// POST /api/bongos/store/modules/:key/delist — a Metic+ person takes a module out
|
|
460
|
+
// of the store (task 1003797, ADR 0338 D2). It leaves the catalog, cannot be newly
|
|
461
|
+
// acquired, takes no new versions and serves no further downloads: every store
|
|
462
|
+
// read already refuses a delisted key, so this flips the one status. What it never
|
|
463
|
+
// does: revoke an entitlement, reach into an instance that already holds the
|
|
464
|
+
// module (that copy keeps working; `bongos upgrade` and the hall tell the holder
|
|
465
|
+
// no more versions will come), or touch a score (ADR 0359). A reason is required;
|
|
466
|
+
// the audit middleware records it with who asked. There is no relist route yet.
|
|
467
|
+
router.post('/store/modules/:key/delist', auth.requireBuilder, auth.requirePermission('module.delist'),
|
|
468
|
+
asyncHandler('POST /store/modules/:key/delist', async (req, res) => {
|
|
469
|
+
if (validateOrRespond(req, res, { reason: { type: 'string', maxLength: 2000 } })) return;
|
|
470
|
+
const { key } = req.params;
|
|
471
|
+
if (!KEY_RE.test(key)) {
|
|
472
|
+
return res.fail('bad_module_key', { status: 400, message: 'A module key is lowercase kebab-case.' });
|
|
473
|
+
}
|
|
474
|
+
const reason = String(req.body?.reason || '').trim();
|
|
475
|
+
if (!reason) {
|
|
476
|
+
return res.fail('reason_required', { status: 400, message: 'Say why this module is leaving the store, in reason. People who already hold it keep it.' });
|
|
477
|
+
}
|
|
478
|
+
const r = await delistModule(key);
|
|
479
|
+
if (!r.ok) return res.fail(r.code, { status: r.status, message: r.message });
|
|
480
|
+
log.info({ key, builderId: req.builder.id }, `${key} delisted from the store`);
|
|
481
|
+
res.json({ ok: true, module_key: key, status: r.module.status, delisted_at: r.module.delisted_at });
|
|
482
|
+
}));
|
|
483
|
+
|
|
449
484
|
// GET /api/bongos/store/entitlements and /store/modules/:key/entitlement — the
|
|
450
485
|
// read side of the entitlement record (task 1003813, ADR 0338 D1). Own-scoped
|
|
451
486
|
// like /me/sessions: the holder is ALWAYS the caller (holder_kind 'builder',
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.34'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -58,7 +58,7 @@ test('modules: the row is the oversight row — title with the mono key, clamped
|
|
|
58
58
|
assert.match(js, /<span class="ov-row__title">\$\{escapeHtml\(m\.title \|\| m\.key\)\}<span class="ov-mono">\$\{escapeHtml\(m\.key\)\}<\/span>/, 'the key rides inside the title in mono');
|
|
59
59
|
assert.match(js, /class="ov-row__sub mod-row__desc"/, 'the description is the sub line');
|
|
60
60
|
assert.match(js, /<details class="mod-more"/, 'a details disclosure opens the rest');
|
|
61
|
-
assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}<\/div>/, 'provenance, credit, upkeep
|
|
61
|
+
assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}\$\{delistedFact\(m\)\}<\/div>/, 'provenance, credit, upkeep, (for a store version, task 1004366) the how-to link and (task 1003797) the delisted pill are the facts line');
|
|
62
62
|
assert.match(js, /<div class="ov-row__right mod-row__right">\$\{rightHtml\(m\)\}<\/div>/, 'the right column is one function');
|
|
63
63
|
const css = read('modules.css');
|
|
64
64
|
assert.match(css, /\.mod-row__desc \{[^}]*-webkit-line-clamp: 2/, 'the description clamps to two lines');
|