@bongos/core 1.24.38 → 1.24.40
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 +39 -39
- package/docs/copy-inventory.md +47 -48
- package/docs/copy-registry.json +75 -84
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +601 -808
- package/modules/hall-ui/public/roster.css +10 -0
- package/modules/hall-ui/public/roster.html +4 -1
- package/modules/hall-ui/public/roster.js +168 -9
- package/modules/hall-ui/public/street-desk.js +1 -1
- package/modules/hall-ui/public/street-draw.js +16 -5
- package/modules/hall-ui/public/street-inside.js +16 -0
- package/modules/hall-ui/public/street.css +36 -0
- package/modules/hall-ui/public/street.js +29 -16
- package/modules/hall-ui/public/watch.css +0 -6
- package/modules/hall-ui/public/watch.html +3 -10
- package/modules/hall-ui/public/watch.js +17 -166
- package/modules/hall-ui/records/builders-roster.md +2 -0
- package/modules/hall-ui/records/governor-street.md +12 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/routes/me.js +3 -1
- package/src/module-api.js +1 -1
- package/tests/hall_matter_card.mjs +14 -7
- package/tests/hall_street.mjs +38 -32
- package/tests/hall_street_depth.mjs +8 -6
- package/tests/hall_street_desk.mjs +8 -5
- package/tests/hall_street_inside.mjs +18 -0
- package/tests/rank_tier_single_source.mjs +3 -1
- package/tests/roster_government_columns.mjs +369 -0
- package/tests/watch_sealed_floor.mjs +6 -4
- package/tests/watch_roster_self_row.mjs +0 -296
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Metic+ monitoring view (Archon-only until ADR 0157 delegated the operational
|
|
4
4
|
// permissions to the working rank). One self-contained page that stitches together
|
|
5
5
|
// the oversight surfaces: pending access requests, security (red-team reports +
|
|
6
|
-
// override queue + audit log), the
|
|
6
|
+
// override queue + audit log), the stale-claim tally (the roster itself is the
|
|
7
|
+
// Builders page since task 1005048), per-builder + project-wide
|
|
7
8
|
// review grades, estimation drift, repo health, and a tally of recorded sessions.
|
|
8
9
|
//
|
|
9
10
|
// Note the page floor and the panel floors are no longer identical: the page and
|
|
@@ -165,7 +166,7 @@
|
|
|
165
166
|
// NOTE: loadMarks is the deliberate outlier — its failure state is a plain
|
|
166
167
|
// "empty" message, not "unavailable", so it keeps its own try/catch.
|
|
167
168
|
// sealedText — name the floor the DENIAL reported, never one written here.
|
|
168
|
-
// Each `sealedMsg` below is the SUBJECT clause only ("The
|
|
169
|
+
// Each `sealedMsg` below is the SUBJECT clause only ("The audit log is not visible
|
|
169
170
|
// at your rank."); the rank sentence in front of it is composed from the 403.
|
|
170
171
|
// That is what keeps this page from telling a builder a permission model the
|
|
171
172
|
// server stopped implementing, which is the whole of task 1003449 — and why the
|
|
@@ -865,164 +866,19 @@
|
|
|
865
866
|
}
|
|
866
867
|
|
|
867
868
|
// =========================================================================
|
|
868
|
-
// 3.
|
|
869
|
+
// 3. STALE CLAIMS — a tally only (task 1005048)
|
|
869
870
|
// =========================================================================
|
|
870
871
|
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const isViewer = (b) => viewerId != null && b && b.id != null && String(b.id) === String(viewerId);
|
|
882
|
-
|
|
883
|
-
function renderRoster(data, target) {
|
|
884
|
-
const builders = Array.isArray(data.builders) ? data.builders : [];
|
|
885
|
-
setAttention('att-stale', builders.filter((b) => b.stale_claim).length);
|
|
886
|
-
target.innerHTML = '';
|
|
887
|
-
if (!builders.length) {
|
|
888
|
-
target.innerHTML = emptyStateHtml('No builders on the roster.');
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
ledger(target, {
|
|
892
|
-
columns: [
|
|
893
|
-
'Builder', 'Rank',
|
|
894
|
-
{ label: (window.OTBBranding?.currencyLabel?.({ caps: true }) ?? 'Credits'), class: 'num' },
|
|
895
|
-
{ label: 'Karma', class: 'num' }, { label: 'Active claims', class: 'num' },
|
|
896
|
-
'Standing', 'Action',
|
|
897
|
-
],
|
|
898
|
-
pager: makePager('watch-roster', 'otb.watch.rosterPerPage'),
|
|
899
|
-
rowHtml: (b) => {
|
|
900
|
-
// THE VIEWER'S OWN ROW OFFERS NOTHING (task 1003720, re-filed from 1002951
|
|
901
|
-
// — "I cannot press offboard", reported by the owner from Discord).
|
|
902
|
-
//
|
|
903
|
-
// The roster returns every builder, so the reader's own row is in it. Both
|
|
904
|
-
// routes behind that row refuse a self-change OUTRIGHT and always will:
|
|
905
|
-
// PATCH /builders/:id/rank answers cannot_change_own_rank and
|
|
906
|
-
// /builders/:id/status answers cannot_change_own_status — the guard that
|
|
907
|
-
// stops the last Archon demoting or offboarding themselves and locking
|
|
908
|
-
// everyone out. The row painted a live rank <select> and a live Offboard
|
|
909
|
-
// pill anyway, so from the owner's seat the page carried a control that
|
|
910
|
-
// could only ever answer with a raw refusal slug in a toast. That is what
|
|
911
|
-
// "I cannot press offboard" describes.
|
|
912
|
-
//
|
|
913
|
-
// This is not a SOFT gate (the primer's acknowledge control, which opens
|
|
914
|
-
// once you have read enough and so stays in the tab order as
|
|
915
|
-
// aria-disabled) — it is an action that can never open for this reader.
|
|
916
|
-
// So the row renders no control at all and the section's foot line says
|
|
917
|
-
// why, rather than painting a dead one.
|
|
918
|
-
const self = isViewer(b);
|
|
919
|
-
const opts = RANK_OPTIONS.slice();
|
|
920
|
-
if (b.rank && !opts.includes(b.rank)) opts.unshift(b.rank);
|
|
921
|
-
const rankCell = self
|
|
922
|
-
? `<span class="watch-self">${escapeHtml(rankLabel(b.rank))}</span>`
|
|
923
|
-
: `<select data-roster-rank data-id="${escapeHtml(b.id)}" data-current="${escapeHtml(b.rank || '')}" aria-label="Rank">` +
|
|
924
|
-
opts.map((rk) => `<option value="${escapeHtml(rk)}"${rk === b.rank ? ' selected' : ''}>${escapeHtml(rankLabel(rk))}</option>`).join('') +
|
|
925
|
-
'</select>';
|
|
926
|
-
const isInactive = String(b.status) === 'inactive';
|
|
927
|
-
const statusBtn = isInactive
|
|
928
|
-
? ghost(`data-roster-status="active" data-id="${escapeHtml(b.id)}"`, 'Reactivate')
|
|
929
|
-
: ghost(`data-roster-status="inactive" data-id="${escapeHtml(b.id)}"`, 'Offboard', true);
|
|
930
|
-
const actionCell = self
|
|
931
|
-
? '<span class="watch-self">That\'s you</span>'
|
|
932
|
-
: `<div class="watch-actions">${statusBtn}</div>`;
|
|
933
|
-
const claims = num(b.active_claim_count);
|
|
934
|
-
const staleFlag = b.stale_claim ? ' <span class="badge badge--sev-medium">stale</span>' : '';
|
|
935
|
-
// task 1467: the roster name links to the builder's public profile page.
|
|
936
|
-
const rosterName = escapeHtml(b.display_name || b.github_login || ('#' + b.id));
|
|
937
|
-
const rosterNameCell = b.id != null
|
|
938
|
-
? `<a class="lb-name" href="/profile?id=${encodeURIComponent(b.id)}">${rosterName}</a>`
|
|
939
|
-
: rosterName;
|
|
940
|
-
return (
|
|
941
|
-
'<tr>' +
|
|
942
|
-
`<td>${rosterNameCell}` +
|
|
943
|
-
(b.github_login ? ` <span class="ov-mono">@${escapeHtml(b.github_login)}</span>` : '') +
|
|
944
|
-
'</td>' +
|
|
945
|
-
`<td>${rankCell}</td>` +
|
|
946
|
-
`<td class="num">${fmtInt(b.total_credits)}</td>` +
|
|
947
|
-
`<td class="num">${fmtInt(b.karma)}</td>` +
|
|
948
|
-
`<td class="num${b.stale_claim ? ' warn' : ''}">${fmtInt(claims)}${staleFlag}</td>` +
|
|
949
|
-
`<td><span class="badge badge--status">${escapeHtml(b.status || '—')}</span></td>` +
|
|
950
|
-
`<td>${actionCell}</td>` +
|
|
951
|
-
'</tr>'
|
|
952
|
-
);
|
|
953
|
-
},
|
|
954
|
-
}).render(builders);
|
|
955
|
-
|
|
956
|
-
// The one asymmetry in the table, stated once under it rather than repeated
|
|
957
|
-
// on the row. Appended as an ELEMENT, not with `innerHTML +=`: re-parsing the
|
|
958
|
-
// body would drop the pager's own wiring. The rank is written from
|
|
959
|
-
// rankLabel(), never a hardcoded "Archon", so the floor cannot drift.
|
|
960
|
-
if (builders.some(isViewer)) {
|
|
961
|
-
const note = document.createElement('p');
|
|
962
|
-
note.className = 'ov-foot';
|
|
963
|
-
note.textContent = `Your own row carries no controls. Nobody can change their own rank or standing — that guard is what stops the last ${rankLabel('archon')} locking everyone out — so another ${rankLabel('archon')} does it.`;
|
|
964
|
-
target.appendChild(note);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Rank change via the <select>. Bespoke: a 'change' listener that rolls the
|
|
968
|
-
// select back to its previous value on failure, delegated so a page flip
|
|
969
|
-
// cannot strand a row without one.
|
|
970
|
-
if (!target['wired:rank']) {
|
|
971
|
-
target['wired:rank'] = true;
|
|
972
|
-
target.addEventListener('change', async (e) => {
|
|
973
|
-
const sel = e.target.closest('[data-roster-rank]');
|
|
974
|
-
if (!sel || !target.contains(sel)) return;
|
|
975
|
-
const id = sel.getAttribute('data-id');
|
|
976
|
-
const rank = sel.value;
|
|
977
|
-
const prev = sel.getAttribute('data-current');
|
|
978
|
-
if (rank === prev) return;
|
|
979
|
-
sel.disabled = true;
|
|
980
|
-
try {
|
|
981
|
-
await patchJson(`/builders/${encodeURIComponent(id)}/rank`, { rank });
|
|
982
|
-
toast(`Rank set to ${rankLabel(rank)}.`);
|
|
983
|
-
await loadRoster();
|
|
984
|
-
} catch (err) {
|
|
985
|
-
if (err && err.kind === '401') return;
|
|
986
|
-
sel.value = prev; // roll back the select on failure
|
|
987
|
-
sel.disabled = false;
|
|
988
|
-
toast(`Could not change rank: ${err.message || err}`);
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Status change (offboard / reactivate). Bespoke: it gathers an offboarding
|
|
994
|
-
// reason via window.prompt BEFORE disabling, so not delegate().
|
|
995
|
-
if (!target['wired:status']) {
|
|
996
|
-
target['wired:status'] = true;
|
|
997
|
-
target.addEventListener('click', async (e) => {
|
|
998
|
-
const btn = e.target.closest('[data-roster-status]');
|
|
999
|
-
if (!btn || !target.contains(btn)) return;
|
|
1000
|
-
const id = btn.getAttribute('data-id');
|
|
1001
|
-
const status = btn.getAttribute('data-roster-status');
|
|
1002
|
-
let reason = null;
|
|
1003
|
-
if (status === 'inactive') {
|
|
1004
|
-
reason = window.prompt('Reason for offboarding (optional):', '') || null;
|
|
1005
|
-
}
|
|
1006
|
-
btn.disabled = true;
|
|
1007
|
-
try {
|
|
1008
|
-
await patchJson(`/builders/${encodeURIComponent(id)}/status`, { status, reason });
|
|
1009
|
-
toast(status === 'inactive' ? 'Builder offboarded.' : 'Builder reactivated.');
|
|
1010
|
-
await loadRoster();
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
if (err && err.kind === '401') return;
|
|
1013
|
-
btn.disabled = false;
|
|
1014
|
-
toast(`Could not change standing: ${err.message || err}`);
|
|
1015
|
-
}
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
function loadRoster() {
|
|
1021
|
-
return section($('#roster-body'), () => fetchJson('/builders/roster'), renderRoster, {
|
|
1022
|
-
sealedMsg: 'The roster is not visible at your rank.',
|
|
1023
|
-
errMsg: 'Could not load the roster.',
|
|
1024
|
-
name: 'roster',
|
|
1025
|
-
});
|
|
872
|
+
// The roster that lived here is the Builders page now (owner decision on idea
|
|
873
|
+
// 1001313: one list of builders). The attention tile keeps its count, read
|
|
874
|
+
// from the same route, and links to that list filtered to the stale claims.
|
|
875
|
+
function loadStaleCount() {
|
|
876
|
+
return fetchJson('/builders/roster')
|
|
877
|
+
.then((data) => {
|
|
878
|
+
const builders = Array.isArray(data && data.builders) ? data.builders : [];
|
|
879
|
+
setAttention('att-stale', builders.filter((b) => b.stale_claim).length);
|
|
880
|
+
})
|
|
881
|
+
.catch(() => setAttention('att-stale', null));
|
|
1026
882
|
}
|
|
1027
883
|
|
|
1028
884
|
// =========================================================================
|
|
@@ -1411,7 +1267,7 @@
|
|
|
1411
1267
|
|
|
1412
1268
|
function revealSections() {
|
|
1413
1269
|
[
|
|
1414
|
-
'watch-attention', 'watch-tabs', 'gate-scroll', 'watch-scroll',
|
|
1270
|
+
'watch-attention', 'watch-tabs', 'gate-scroll', 'watch-scroll',
|
|
1415
1271
|
'marks-scroll', 'mason-marks-scroll', 'surveyor-scroll', 'ledger-scroll', 'sessions-scroll',
|
|
1416
1272
|
].forEach((id) => {
|
|
1417
1273
|
const el = document.getElementById(id);
|
|
@@ -1441,11 +1297,6 @@
|
|
|
1441
1297
|
if (!res.ok) throw new Error(`me → ${res.status}`);
|
|
1442
1298
|
const payload = res.data;
|
|
1443
1299
|
builder = (payload && payload.builder) ? payload.builder : payload;
|
|
1444
|
-
// Kept for the roster, which must know which row is the reader's own
|
|
1445
|
-
// (task 1003720). Null when /me answered without one — isViewer then
|
|
1446
|
-
// matches nothing, so the roster degrades to the old every-row-acts shape
|
|
1447
|
-
// rather than marking the wrong row as yours.
|
|
1448
|
-
viewerId = (builder && builder.id != null) ? builder.id : null;
|
|
1449
1300
|
} catch (err) {
|
|
1450
1301
|
console.error('[watch] failed to read viewer', err);
|
|
1451
1302
|
showSealed('Could not check your access. Reload to try again.', 'Could not load');
|
|
@@ -1454,7 +1305,7 @@
|
|
|
1454
1305
|
}
|
|
1455
1306
|
|
|
1456
1307
|
// ADR 0157: Watch sits at the `page.view.government` floor (metic), and every panel
|
|
1457
|
-
// it reads (
|
|
1308
|
+
// it reads (stale claims, security reports, audit log, sessions, box costs) delegated to
|
|
1458
1309
|
// metic with it. Cosmetic — requireGovernmentPage + the route gates are the wall.
|
|
1459
1310
|
if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
|
|
1460
1311
|
showSealed(`Available from the ${rankLabel('metic')} rank up. Your current rank does not grant access.`);
|
|
@@ -1469,7 +1320,7 @@
|
|
|
1469
1320
|
loadEngagementDocs();
|
|
1470
1321
|
loadOverrides();
|
|
1471
1322
|
loadAudit();
|
|
1472
|
-
|
|
1323
|
+
loadStaleCount();
|
|
1473
1324
|
loadMarks();
|
|
1474
1325
|
loadMasonMarks();
|
|
1475
1326
|
loadSurveyor();
|
|
@@ -37,3 +37,5 @@ The owner's Studio idea 1001297 (a circuit-trace image as a table divider; mock
|
|
|
37
37
|
- **The pseudo-elements sit on the row's first `td`, never on the `tr`.** On the `tr` they became two extra table cells and pushed every cell in the row two columns right (seen in the first render). The `tr` only carries `position: relative`, so the `td`'s absolute children size against the whole row.
|
|
38
38
|
- **Two masks, two tokens.** Traces are `--ink-faint` and the orange nodes `--accent`, each shown through an SVG mask, so light and dark follow the world with no hard-coded colour. Left cap, right cap and the stretching hairline are three layers of one mask.
|
|
39
39
|
- **The empty-state row has no trace** (`:not(:has(.ledger__empty))`). Checked at desktop and 375px, light and dark: the table scrolls inside its card on a phone and the divider spans it; the page does not scroll sideways.
|
|
40
|
+
|
|
41
|
+
**Since task 1005048 it is the one list.** Watch's own roster folded in: a viewer the `GET /builders/roster` route lets in also sees active claims, the stale flag, standing and the rank/offboard controls, and the route's refusal leaves this page exactly the public list described above.
|
|
@@ -106,3 +106,15 @@ Owner decision 2026-10-04: the street should read at a glance without words, so
|
|
|
106
106
|
- **The evening view is unchanged** ("nothing waits on you." and its line still show when nothing needs you).
|
|
107
107
|
|
|
108
108
|
Tests: `tests/hall_street.mjs` asserts the model has no hint field, the module exports no `HINT`, and none of the street's four files carries the line; putting the markup or the constant back reds it.
|
|
109
|
+
|
|
110
|
+
## The hall is for work: the street as a square, the building as a place to work (owner, 2026-11-04)
|
|
111
|
+
|
|
112
|
+
The owner's direction: the builders' hall is where you go to do things, not to look at them. No menu on menu, no pop-ups (go to the place), no page header or footer that a web app does not need. On the street that meant:
|
|
113
|
+
|
|
114
|
+
- **The street is the governance square.** The working areas are the city's own buildings, so no district stands on the street: `streetModel()` draws one only with `districts: true` (the kit's tests still hold that row to the prototype), and without it the civic row is centred on the artboard. A wide window scales the street to the row (`FOCUS_W`, 1120), not the whole 1440 artboard, so the buildings are as large as the height allows and the haze either side is cropped; a phone still scrolls it sideways and opens at the first building.
|
|
115
|
+
- **The header strip and the six readings are gone** (`.street-top`, `.street-signs` are not displayed; the markup stays, so the counts, the deck link and the readings still compute). The shell's top bar names the page and the buildings carry their own state lines.
|
|
116
|
+
- **A pin takes you into its building** (`#building=<key>`), where each matter is a desk row you act on. It no longer opens the short card; the card is reached only by a pin with no building, and retiring it is a follow-up.
|
|
117
|
+
- **Inside a building the drawing is the floor picker.** On a wide window (`.inside--work`) the interior is laid out, not scaled as one 1440 x 708 picture: the section is cropped to the building (`CROP` in `fitInside`, 590 x 580 of the 800 x 708 section), at 0.5 to 0.72 so a floor stays a 24 px target, in its own column; the chosen floor's desk takes the rest of the window and scrolls on its own. The building's name sits on one line, then the desk, then the government layer, who holds it and how it decides, then the record. A phone keeps its column with the same order. A district keeps its scaled board.
|
|
118
|
+
- **A desk row is one full line.** The board's 20 / 26 / 24 caps were sized for a desk in a corner of the interior; `LIMITS` is now name 90 (14 words), fact 60, facts 48.
|
|
119
|
+
|
|
120
|
+
Tests: `tests/hall_street.mjs` (nine doors and no district on the page, a pin goes inside and fills no card, the row scaled and centred, a phone opening at the first building), `tests/hall_street_inside.mjs` (the cropped picker, never under half size, the board not scaled, a district untouched), `tests/hall_street_depth.mjs` (with no plot to fly from every agent rides the road), `tests/hall_street_desk.mjs` (the new limits).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.40",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.40",
|
|
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.24.
|
|
3
|
+
"version": "1.24.40",
|
|
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
|
@@ -11191,5 +11191,17 @@
|
|
|
11191
11191
|
"id": "1005071",
|
|
11192
11192
|
"text": "The hall sidebar is now seven labelled sections (Work, Make, Community, You, Govern, Run the project, Help), and every craft can reach Help."
|
|
11193
11193
|
}
|
|
11194
|
+
],
|
|
11195
|
+
"1.24.39": [
|
|
11196
|
+
{
|
|
11197
|
+
"id": "1005077",
|
|
11198
|
+
"text": "The governor street is now a work-first square: the working areas left the street, the extra header and footer are gone, a pin takes you straight into its building, and inside a building the drawing is a compact floor picker s"
|
|
11199
|
+
}
|
|
11200
|
+
],
|
|
11201
|
+
"1.24.40": [
|
|
11202
|
+
{
|
|
11203
|
+
"id": "1005048",
|
|
11204
|
+
"text": "The Builders page is now the one list of everyone who builds here. If you hold a government seat you also see each builder's active claims, any stale claim, their standing, and the controls to change a rank or offboard someone"
|
|
11205
|
+
}
|
|
11194
11206
|
]
|
|
11195
11207
|
}
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -146,7 +146,9 @@ const discordIsLinked = (builderId) =>
|
|
|
146
146
|
// buttons for a viewer who cannot vote on ideas, without re-deriving it from rank.
|
|
147
147
|
// `blocker.review` (task 1004824): the Blockers page and its overlay offer Resolve and
|
|
148
148
|
// Link only to a builder who holds it; POST /blockers/:id/{resolve,link} stays the wall.
|
|
149
|
-
|
|
149
|
+
// `builder.roster.read` (task 1005048): the Builders page asks GET /builders/roster
|
|
150
|
+
// for its government columns only of a viewer who holds it; the route stays the wall.
|
|
151
|
+
const NAV_PERMISSION_ATOMS = Object.freeze(['board.vote.cast', 'core.pin.move', 'idea.triage', 'board.item.open', 'idea.vote', 'blocker.review', 'builder.roster.read']);
|
|
150
152
|
|
|
151
153
|
// navPermissionsFor(builderId) → { '<atom>': boolean } over NAV_PERMISSION_ATOMS,
|
|
152
154
|
// or null when authority could not be established (government off, port missing,
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.40'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -290,9 +290,16 @@ const BASE = (d) => ({
|
|
|
290
290
|
'/api/bongos/government/constitution': { board: { membership: 'rank:metic+', pass_rule: 'unanimous', window_minutes: 1440, close_early_on_full_turnout: true } },
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
|
|
293
|
+
// A pin goes into its building now (owner, 2026-11-04: no pop-ups); the card
|
|
294
|
+
// still opens for a pin with no building, so these tests open it directly.
|
|
295
|
+
const openCard = (p) => {
|
|
296
|
+
const pin = p.byId.get('streetPins').children[0];
|
|
297
|
+
p.window.GovStreetPage.openCard(pin.attrs['data-matter'], pin, pin.attrs['data-matters'].split(' '));
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
test('the full card: the ballot is read, then the seats, counsel and acts are drawn as text', async () => {
|
|
294
301
|
const p = await bootPage({ feeds: BASE(docket([matter({ title: '<b>x</b>' })])) });
|
|
295
|
-
p
|
|
302
|
+
openCard(p);
|
|
296
303
|
await settle(); await settle();
|
|
297
304
|
assert.ok(p.calls.some((c) => c.url === '/api/bongos/government/board/items'), 'the room is read when a sitting opens');
|
|
298
305
|
const body = p.byId.get('streetCardBody');
|
|
@@ -308,7 +315,7 @@ test('vote for: one POST to the vote route, a toast, the docket re-read, and the
|
|
|
308
315
|
const feeds = BASE((() => { const d0 = docket([matter()]); return () => (reads++ === 0 ? d0 : after); })());
|
|
309
316
|
feeds['/api/bongos/government/docket'] = (() => { const d0 = docket([matter()]); return () => (reads++ === 0 ? d0 : after); })();
|
|
310
317
|
const p = await bootPage({ feeds, posts: { 'POST /api/bongos/government/board/items/41/votes': { ok: true, vote: {}, already_voted: false, karma_delta: 1 } } });
|
|
311
|
-
p
|
|
318
|
+
openCard(p);
|
|
312
319
|
await settle(); await settle();
|
|
313
320
|
byAct(p.byId.get('streetCardBody'), 'for').fire('click');
|
|
314
321
|
await settle(); await settle(); await settle();
|
|
@@ -324,7 +331,7 @@ test('vote for: one POST to the vote route, a toast, the docket re-read, and the
|
|
|
324
331
|
|
|
325
332
|
test('object: "no" opens the desk row\'s one-tap reasons, and a tap puts the reason and the part on the record', async () => {
|
|
326
333
|
const p = await bootPage({ feeds: BASE(docket([matter()])), posts: { 'POST /api/bongos/government/board/items/41/votes': { ok: true, vote: {}, item_closed: true, outcome: 'returned' } } });
|
|
327
|
-
p
|
|
334
|
+
openCard(p);
|
|
328
335
|
await settle(); await settle();
|
|
329
336
|
const body = () => p.byId.get('streetCardBody');
|
|
330
337
|
assert.equal(byAct(body(), 'object').textContent, D.ACT_WORDS.object, 'the desk row\'s word');
|
|
@@ -341,7 +348,7 @@ test('object: "no" opens the desk row\'s one-tap reasons, and a tap puts the rea
|
|
|
341
348
|
|
|
342
349
|
test('a refused act says why and records nothing; abstain sends nothing at all', async () => {
|
|
343
350
|
const p = await bootPage({ feeds: BASE(docket([matter()])), posts: { 'POST /api/bongos/government/board/items/41/votes': { __status: 403, error: { code: 'not_board_member' } } } });
|
|
344
|
-
p
|
|
351
|
+
openCard(p);
|
|
345
352
|
await settle(); await settle();
|
|
346
353
|
byAct(p.byId.get('streetCardBody'), 'for').fire('click');
|
|
347
354
|
await settle(); await settle();
|
|
@@ -361,7 +368,7 @@ test('release: the override is approved, the card closes, Release rises, and wit
|
|
|
361
368
|
feeds['/api/bongos/government/docket'] = () => (reads++ === 0 ? docket([ov]) : docket([]));
|
|
362
369
|
const p = await bootPage({ feeds, posts: { 'POST /api/bongos/override-requests/9/decide': { ok: true } } });
|
|
363
370
|
assert.equal(p.byId.get('streetEvening').hidden, true);
|
|
364
|
-
p
|
|
371
|
+
openCard(p);
|
|
365
372
|
byAct(p.byId.get('streetCardBody'), 'release').fire('click');
|
|
366
373
|
await settle(); await settle(); await settle();
|
|
367
374
|
assert.deepEqual(p.calls.filter((c) => c.method !== 'GET').map((c) => [c.method, c.url, c.body]), [['POST', '/api/bongos/override-requests/9/decide', { decision: 'approve' }]]);
|
|
@@ -476,7 +483,7 @@ test('more expands the same card in place: the full text, who and when, and the
|
|
|
476
483
|
const { d } = SOURCE_FIXTURES();
|
|
477
484
|
d.matters = d.matters.filter((m) => m.source === 'blockers');
|
|
478
485
|
const p = await bootPage({ feeds: { ...BASE(d) } });
|
|
479
|
-
p
|
|
486
|
+
openCard(p);
|
|
480
487
|
await settle(); await settle();
|
|
481
488
|
const body = () => p.byId.get('streetCardBody');
|
|
482
489
|
const toggle = () => byAct(body(), 'more');
|
package/tests/hall_street.mjs
CHANGED
|
@@ -72,7 +72,9 @@ function docket(matters = [], over = {}) {
|
|
|
72
72
|
}
|
|
73
73
|
const goal = (id, status = 'open', title = `Goal ${id}`) => ({ id: String(id), title, status });
|
|
74
74
|
const rollup = (id, c) => ({ goal_id: String(id), done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, ...c });
|
|
75
|
-
|
|
75
|
+
// The kit is held to the prototype's row, districts and all (`districts: true`); the
|
|
76
|
+
// page itself draws the civic row alone (owner, 2026-11-04), pinned further down.
|
|
77
|
+
const model = (over = {}) => S.streetModel({ docket: docket(), goals: [], rollups: [], mode: 'dark', now: NOW, districts: true, ...over });
|
|
76
78
|
|
|
77
79
|
// ── the drawing is the prototype's ───────────────────────────────────────────
|
|
78
80
|
|
|
@@ -299,7 +301,7 @@ test('the collision pass places in tier order: when only one of two can show, th
|
|
|
299
301
|
});
|
|
300
302
|
|
|
301
303
|
test('no docket: no pins, the readings say so, and nothing is invented', () => {
|
|
302
|
-
const m = S.streetModel({ docket: null, goals: [goal(1)], rollups: [], mode: 'dark', now: NOW });
|
|
304
|
+
const m = S.streetModel({ docket: null, goals: [goal(1)], rollups: [], mode: 'dark', now: NOW, districts: true });
|
|
303
305
|
assert.equal(m.pins.length, 0);
|
|
304
306
|
assert.equal(m.crates.length, 0);
|
|
305
307
|
assert.ok(m.signs.every((s) => s.figure === '—' && !s.hot && s.line === S.FALLBACK_LINE));
|
|
@@ -617,32 +619,30 @@ test('the page reads the four feeds and draws the docket\'s pins, the readings a
|
|
|
617
619
|
assert.equal(p.byId.get('hudMoving').textContent, '2');
|
|
618
620
|
assert.equal(p.byId.get('hudShare').textContent, '$24');
|
|
619
621
|
assert.equal(p.byId.get('hudDot').className, 'street-hud__dot street-hud__dot--hot');
|
|
620
|
-
assert.equal(p.byId.get('streetDoors').children.length,
|
|
622
|
+
assert.equal(p.byId.get('streetDoors').children.length, 9, 'the nine civic buildings, each a door; no district stands on the street (owner, 2026-11-04)');
|
|
623
|
+
assert.ok(p.byId.get('streetDoors').children.every((a) => !/district=/.test(a.attrs.href)), 'no door to a working area');
|
|
621
624
|
assert.equal(p.byId.get('streetHome').attrs.href, '/builders/city');
|
|
622
625
|
});
|
|
623
626
|
|
|
624
|
-
|
|
625
|
-
|
|
627
|
+
// Owner, 2026-11-04: no pop-ups, go to the place. A pin takes you into its
|
|
628
|
+
// building, where each matter is a desk row you act on; it opens no card.
|
|
629
|
+
test('a pin takes you into its building and opens no card', async () => {
|
|
630
|
+
const p = await bootPage({ feeds: FEEDS([matter({ id: 'board:1', building: 'charter', title: 'A vote', href: '/builders/board-room' })]) });
|
|
626
631
|
const pin = p.byId.get('streetPins').children[0];
|
|
632
|
+
assert.equal(pin.attrs['data-building'], 'charter');
|
|
627
633
|
pin.fire('click');
|
|
628
|
-
|
|
629
|
-
assert.equal(
|
|
630
|
-
assert.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
assert.equal(card.hidden, true);
|
|
637
|
-
assert.equal(e.prevented, true, 'the press is used, so nothing else answers it');
|
|
638
|
-
assert.equal(p.focused.el.attrs['data-matter'], 'board:1');
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
test('the card pager: a pin with several matters opens the most important with "1 of N", and previous/next reach every matter there', async () => {
|
|
634
|
+
assert.equal(p.window.location.hash, 'building=charter', 'the address names the building, so its inside opens');
|
|
635
|
+
assert.equal(p.byId.get('streetCardTitle').textContent, '', 'no card is filled');
|
|
636
|
+
assert.notEqual(p.byId.get('streetPins').children[0].attrs['aria-expanded'], 'true', 'and the pin does not say one is open');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// The card itself still works for the code paths that open it (a pin with no
|
|
640
|
+
// building); its pager is held here by opening it directly.
|
|
641
|
+
test('the card pager: a card over several matters opens the most important with "1 of N", and previous/next reach every matter there', async () => {
|
|
642
642
|
const ms = [matter({ id: 'blockers:1', state: 'moving', title: 'Third' }), matter({ id: 'blockers:2', state: 'needs', title: 'First' }), matter({ id: 'blockers:3', state: 'needs', title: 'Second' }), matter({ id: 'blockers:4', state: 'settled', title: 'Fourth' })];
|
|
643
643
|
const p = await bootPage({ feeds: FEEDS(ms) });
|
|
644
644
|
const pin = p.byId.get('streetPins').children.find((s) => s.attrs['data-building'] === 'planning');
|
|
645
|
-
pin.
|
|
645
|
+
p.window.GovStreetPage.openCard(pin.attrs['data-matter'], pin, pin.attrs['data-matters'].split(' '));
|
|
646
646
|
const body = p.byId.get('streetCardBody');
|
|
647
647
|
const pager = () => body.children[0];
|
|
648
648
|
const btn = (dir) => pager().children.find((c) => c.attrs['data-act'] === 'page-' + dir);
|
|
@@ -664,7 +664,8 @@ test('the card pager: a pin with several matters opens the most important with "
|
|
|
664
664
|
assert.equal(p.focused.el.attrs['data-building'], 'planning', 'Escape returns focus to the pin');
|
|
665
665
|
// one matter: no pager
|
|
666
666
|
const q = await bootPage({ feeds: FEEDS([matter({ id: 'board:1', building: 'charter' })]) });
|
|
667
|
-
q.byId.get('streetPins').children[0]
|
|
667
|
+
const one = q.byId.get('streetPins').children[0];
|
|
668
|
+
q.window.GovStreetPage.openCard(one.attrs['data-matter'], one, one.attrs['data-matters'].split(' '));
|
|
668
669
|
assert.ok(!q.byId.get('streetCardBody').children.some((c) => c.className === 'mcard-pager'), 'one matter, no pager');
|
|
669
670
|
});
|
|
670
671
|
|
|
@@ -737,17 +738,22 @@ test('arriving through a door marks that building, and a failed docket says so',
|
|
|
737
738
|
assert.equal(q.byId.get('hudShare').textContent, '—');
|
|
738
739
|
});
|
|
739
740
|
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
|
|
741
|
+
// Owner, 2026-11-04 (supersedes task 1004718's foot): the readings left the
|
|
742
|
+
// page, so a wide window scales the street to its civic row, not the whole
|
|
743
|
+
// artboard, crops the haze either side, and centres the street in its stage.
|
|
744
|
+
test('a wide window scales the street to its civic row and centres it; a phone scrolls it from the first building', async () => {
|
|
743
745
|
const p = await bootPage({ feeds: FEEDS([]), width: 1440 });
|
|
744
|
-
const win = p.byId.get('streetWindow'), sizer = p.byId.get('streetSizer');
|
|
745
|
-
win.clientHeight =
|
|
746
|
-
p.key('resize', {});
|
|
747
|
-
const h = parseFloat(sizer.style.height);
|
|
748
|
-
assert.ok(h < 900, 'the street is width-bound here');
|
|
749
|
-
assert.equal(win.style.flex, '0 0 ' + h + 'px', 'the window shrinks to the street, so the readings sit right under it');
|
|
750
|
-
win.clientHeight = 300;
|
|
746
|
+
const win = p.byId.get('streetWindow'), sizer = p.byId.get('streetSizer'), board = p.byId.get('streetBoard');
|
|
747
|
+
win.clientWidth = 1376; win.clientHeight = 844;
|
|
751
748
|
p.key('resize', {});
|
|
752
|
-
|
|
749
|
+
const s = Number(board.style['--street-scale']);
|
|
750
|
+
assert.ok(s > 1376 / 1440, 'larger than the whole artboard would allow');
|
|
751
|
+
assert.equal(win.style.flex, '', 'the window keeps the stage, so the street sits centred in it');
|
|
752
|
+
assert.equal(win.style.overflowX, 'hidden', 'no sideways scroll on a wide window');
|
|
753
|
+
assert.equal(parseFloat(sizer.style.width), 1376, 'the sizer is the window');
|
|
754
|
+
assert.ok(parseFloat(board.style.left) < 0, 'the artboard is centred, its haze cropped either side');
|
|
755
|
+
const q = await bootPage({ feeds: FEEDS([]), width: 390 });
|
|
756
|
+
const qwin = q.byId.get('streetWindow');
|
|
757
|
+
assert.equal(qwin.style.overflowX, '', 'a phone scrolls the street sideways');
|
|
758
|
+
assert.ok(qwin.scrollLeft > 0, 'starting at the first building, not the empty haze before it');
|
|
753
759
|
});
|
|
@@ -70,7 +70,7 @@ function feeds(over = {}) {
|
|
|
70
70
|
function living(f) { return L.livingModel({ ...f, now: NOW }); }
|
|
71
71
|
function street(over = {}, mode = 'dark') {
|
|
72
72
|
const f = feeds(over);
|
|
73
|
-
return S.streetModel({ docket: f.docket, goals: f.goals.goals, rollups: f.rollups.rollups, mode, now: NOW, depth: true, living: f.living === null ? null : living(f), roster: f.roster });
|
|
73
|
+
return S.streetModel({ docket: f.docket, goals: f.goals.goals, rollups: f.rollups.rollups, mode, now: NOW, depth: true, living: f.living === null ? null : living(f), roster: f.roster, districts: true });
|
|
74
74
|
}
|
|
75
75
|
const openGoals = () => fx('goals').goals.filter((g) => g.status === 'open');
|
|
76
76
|
|
|
@@ -338,7 +338,7 @@ test('an overlay handed in changes nothing; the Leads not read, no Lead, capsule
|
|
|
338
338
|
const c = street({ leads: null }).depth;
|
|
339
339
|
assert.deepEqual([c.plates.length, c.capsules.length, c.sites.length], [0, 0, 0], 'no Leads read: no plate, no capsule, no build-out going up');
|
|
340
340
|
// without the living model at all the deep street still draws its real skyline and windows
|
|
341
|
-
const none = S.streetModel({ docket: fx('government__docket'), goals: fx('goals').goals, rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true, living: null, roster: null }).depth;
|
|
341
|
+
const none = S.streetModel({ docket: fx('government__docket'), goals: fx('goals').goals, rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true, living: null, roster: null, districts: true }).depth;
|
|
342
342
|
assert.deepEqual(none.clusters, a.clusters);
|
|
343
343
|
assert.deepEqual(none.windows, street({ leads: null, docket: fx('government__docket') }).depth.windows, 'no living model and the hub as it is light the same, the docket\'s own');
|
|
344
344
|
assert.equal(none.agents, 0);
|
|
@@ -361,7 +361,7 @@ test('street-depth.js: its model half is pure, and the two drawing files require
|
|
|
361
361
|
const before = globalThis.GovStreetDepth;
|
|
362
362
|
delete globalThis.GovStreetDepth;
|
|
363
363
|
try {
|
|
364
|
-
const flat = S.streetModel({ docket: fx('government__docket'), goals: openGoals(), rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true });
|
|
364
|
+
const flat = S.streetModel({ docket: fx('government__docket'), goals: openGoals(), rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true, districts: true });
|
|
365
365
|
assert.equal(flat.depth, undefined);
|
|
366
366
|
} finally { globalThis.GovStreetDepth = before; }
|
|
367
367
|
});
|
|
@@ -500,9 +500,11 @@ test('the page draws the living street: the skyline, a pod per claim, the plates
|
|
|
500
500
|
assert.equal(cls('streetDepthFar', 'street-depth-sil__face').length, 1);
|
|
501
501
|
const claims = fx('tasks__claimable').active_claims.length;
|
|
502
502
|
assert.equal(cls('streetDepthSky', 'street-depth-pod').length + cls('streetDepthFront', 'street-depth-pod').length, claims, 'one pod per agent');
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
503
|
+
// The page stands no district (owner, 2026-11-04), so no claim flies a skylane
|
|
504
|
+
// from its goal's plot: every agent rides the road (the kit's skylanes are held
|
|
505
|
+
// with districts on, above).
|
|
506
|
+
assert.equal(cls('streetDepthSky', 'street-depth-pod').length, 0, 'no plot to fly from');
|
|
507
|
+
assert.equal(cls('streetDepthFront', 'street-depth-pod--road').length, claims, 'every agent on the road');
|
|
506
508
|
assert.equal(cls('streetDepthFront', 'street-depth-plate').length, 9);
|
|
507
509
|
assert.equal(all('streetDepthFront').concat(all('streetDepthSky')).filter((e) => 'data-proto' in e.attrs).length, 0, 'nothing is marked prototype');
|
|
508
510
|
assert.equal(cls('streetDepthFront', 'street-depth-jib').length, 1, 'the live site\'s crane');
|
|
@@ -105,11 +105,14 @@ test('fit() is the board\'s fit(), with a word cap on top', () => {
|
|
|
105
105
|
for (const n of [5, 12, 20]) for (const w of [1, 2, 3]) assert.ok(D.fit(LONG, n, w).length <= n, 'never longer than its cap');
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
// A row's name and fact were the board's 20 / 26 / 24 while the desk was a
|
|
109
|
+
// corner of the interior; the desk takes the window's width now (owner,
|
|
110
|
+
// 2026-11-04), so a row is one full line: name 90 (14 words), fact 60, facts 48.
|
|
111
|
+
test('the limits: a row is one full line (name 90, fact 60, facts 48, three), status 44, record 30 (four), three tiles, ten pips', () => {
|
|
109
112
|
const L = D.LIMITS;
|
|
110
|
-
assert.deepEqual([L.name, L.nameWords, L.fact, L.factLen, L.facts, L.actions, L.status, L.recordLen, L.record, L.tiles, L.tileWords, L.todayTarget], [
|
|
113
|
+
assert.deepEqual([L.name, L.nameWords, L.fact, L.factLen, L.facts, L.actions, L.status, L.recordLen, L.record, L.tiles, L.tileWords, L.todayTarget], [90, 14, 60, 48, 3, 2, 44, 30, 4, 3, 2, 10]);
|
|
111
114
|
const src = fs.readFileSync(path.join(ROOT, 'docs/design/mocks/governor-city/Desks.dc.html'), 'utf8');
|
|
112
|
-
for (const pin of ['
|
|
115
|
+
for (const pin of ['.slice(0, 3)', "this.fit('nothing needs you · ' + F.quiet, 44)", "this.fit(t + ' · now', 30)", '.slice(0, 4)', 'const target = 10']) assert.ok(src.includes(pin), `the board says ${pin}`);
|
|
113
116
|
});
|
|
114
117
|
|
|
115
118
|
test('THE LINE RULE: no row, tile or status any docket produces breaks it, on every floor of every building', () => {
|
|
@@ -140,8 +143,8 @@ test('breaks() names every way a desk can break the rule (so the rule test can f
|
|
|
140
143
|
const good = D.deskModel({ key: 'people', floor: 4, docket: dk, now: NOW });
|
|
141
144
|
assert.deepEqual(D.breaks(good), []);
|
|
142
145
|
const bad = j(good);
|
|
143
|
-
bad.rows[0].name = '
|
|
144
|
-
bad.rows[0].fact = 'x'.repeat(
|
|
146
|
+
bad.rows[0].name = Array(16).fill('word').join(' ');
|
|
147
|
+
bad.rows[0].fact = 'x'.repeat(61);
|
|
145
148
|
bad.rows[0].actions = [{ label: 'a' }, { label: 'b' }, { label: 'c' }];
|
|
146
149
|
bad.rows[0].facts = ['a', 'b', 'c', 'd'];
|
|
147
150
|
bad.tiles = [{ v: '0', l: 'need you' }, { v: '3', l: 'three whole words' }, { v: '4', l: 'ok' }, { v: '5', l: 'ok' }];
|
|
@@ -555,6 +555,24 @@ test('the markup carries the interior: one persistent lift, two flip blocks, the
|
|
|
555
555
|
assert.match(html, /<button type="button" class="inside-textbtn" id="insideAmend"/);
|
|
556
556
|
});
|
|
557
557
|
|
|
558
|
+
// Owner, 2026-11-04: a building is a place to work. On a wide window the
|
|
559
|
+
// drawing is a floor picker cropped to the building, at half size or more (a
|
|
560
|
+
// floor stays a 24 px target), and the desk has the rest of the window; the
|
|
561
|
+
// whole interior is no longer one scaled picture. A district keeps its board.
|
|
562
|
+
test('inside a building the drawing is a cropped floor picker, never under half size, and the board is not scaled', async () => {
|
|
563
|
+
const p = await bootPage({ hash: '#building=planning', feeds: FEEDS([]) });
|
|
564
|
+
const box = p.byId.get('insideArtBox'), art = p.byId.get('insideArt'), board = p.byId.get('insideBoard');
|
|
565
|
+
const k = Number(box.style['--inside-art-scale']);
|
|
566
|
+
assert.ok(k >= 0.5 && k <= 0.72, `picker scale ${k}`);
|
|
567
|
+
assert.equal(board.style['--inside-scale'], '1', 'the interior is laid out, not shrunk as one picture');
|
|
568
|
+
assert.equal(art.style['--inside-art-w'], Math.round(590 * k) + 'px', 'cropped to the building, not the 800 px section');
|
|
569
|
+
assert.equal(box.style['--inside-art-x'], Math.round(-50 * k) + 'px');
|
|
570
|
+
assert.ok(47 * k >= 24, 'a floor stays a 24 px target');
|
|
571
|
+
const d = await bootPage({ hash: '#district=1', feeds: FEEDS([]) });
|
|
572
|
+
assert.equal(d.byId.get('insideArtBox').style['--inside-art-scale'], '1', 'a district keeps its whole board');
|
|
573
|
+
assert.equal(d.byId.get('insideArtBox').style['--inside-art-x'], undefined, 'and is not cropped');
|
|
574
|
+
});
|
|
575
|
+
|
|
558
576
|
test('a building address draws the interior in place of the street, from the docket', async () => {
|
|
559
577
|
const ms = [matter({ id: 'access_requests:1', source: 'access_requests', building: 'people', state: 'needs', title: 'Admit Kai' })];
|
|
560
578
|
const p = await bootPage({ hash: '#building=people', feeds: FEEDS(ms) });
|