@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
|
@@ -95,3 +95,13 @@
|
|
|
95
95
|
-webkit-mask-position: left top, right top;
|
|
96
96
|
mask-position: left top, right top;
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
/* ---- the government columns (task 1005048) -------------------------------
|
|
100
|
+
Watch's roster folded in here. A stale claim takes the danger ink and a
|
|
101
|
+
small badge; the viewer's own row states a fact in the faint ink where the
|
|
102
|
+
others carry a control; the foot line under the table says why. */
|
|
103
|
+
.roster-warn { color: var(--danger); font-weight: 500; }
|
|
104
|
+
.roster-self { color: var(--ink-faint); }
|
|
105
|
+
.badge--sev-medium { color: var(--info); }
|
|
106
|
+
.badge--status { color: var(--ink-faint); }
|
|
107
|
+
.roster-foot { margin: 14px 0 0; font-size: 12.5px; color: var(--ink-faint); max-width: var(--measure); }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Builders — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="Who builds on this project: the roster, ranked by work shipped, with rank, credits, karma and tokens.">
|
|
7
|
+
<meta name="description" content="Who builds on this project: the roster, ranked by work shipped, with rank, credits, karma and tokens — and, for a government seat, active claims and standing.">
|
|
8
8
|
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
<div id="roster-body">
|
|
42
42
|
<div class="hall-loading hall-loading--page" role="status">Loading the roster…</div>
|
|
43
43
|
</div>
|
|
44
|
+
<!-- task 1005048: shown only to a viewer the roster route lets read the
|
|
45
|
+
government columns; says what they are and why one row has no controls. -->
|
|
46
|
+
<p class="roster-foot" id="roster-gov-foot" hidden></p>
|
|
44
47
|
</section>
|
|
45
48
|
</main>
|
|
46
49
|
|
|
@@ -136,6 +136,125 @@
|
|
|
136
136
|
var sortBy = '';
|
|
137
137
|
var role = '';
|
|
138
138
|
|
|
139
|
+
// ---- the government columns (task 1005048, owner decision on idea 1001313) --
|
|
140
|
+
// Watch kept a second list of the same people; it is this page now. A viewer
|
|
141
|
+
// the server lets read GET /builders/roster (the builder.roster.read atom) also
|
|
142
|
+
// sees each builder's active claims, the stale-claim flag and standing, and
|
|
143
|
+
// gets the rank and offboard controls that list carried. For anyone else that
|
|
144
|
+
// read is refused and the page draws the public list unchanged: the wall is
|
|
145
|
+
// the route, never this file.
|
|
146
|
+
var gov = null; // builder id → their GET /builders/roster row, once it answers
|
|
147
|
+
var viewerId = null;
|
|
148
|
+
var claims = '';
|
|
149
|
+
var RANK_OPTIONS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
150
|
+
var client = window.BongosClient
|
|
151
|
+
? window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false })
|
|
152
|
+
: null;
|
|
153
|
+
var toast = function (m) { if (OTB && OTB.toast) OTB.toast(m); };
|
|
154
|
+
// By value: /me and the roster can disagree on string vs number ids (#348).
|
|
155
|
+
function isViewer(b) { return viewerId != null && b && b.id != null && String(b.id) === String(viewerId); }
|
|
156
|
+
function govOf(b) { return gov && b.id != null ? gov[String(b.id)] || null : null; }
|
|
157
|
+
// Resolves to the government rows, or null for every refusal and failure.
|
|
158
|
+
// The roster read is asked only of a viewer whose /me map says they hold
|
|
159
|
+
// builder.roster.read, so a stranger or a builder without the seat costs this
|
|
160
|
+
// public page no extra round trip. /me itself is the shell's read, shared on
|
|
161
|
+
// window.__hallReads (the gov-feeds.js precedent); asked here only without it.
|
|
162
|
+
function readMe() {
|
|
163
|
+
var shared = window.__hallReads && window.__hallReads['/me'];
|
|
164
|
+
if (shared) return Promise.resolve(shared);
|
|
165
|
+
return client.request('GET', '/api/bongos/me').then(function (res) { return res && res.ok ? res.data : null; });
|
|
166
|
+
}
|
|
167
|
+
function loadGov() {
|
|
168
|
+
if (!client) return Promise.resolve(null);
|
|
169
|
+
return readMe().then(function (me) {
|
|
170
|
+
var perms = me && me.permissions;
|
|
171
|
+
if (!perms || perms['builder.roster.read'] !== true) return null;
|
|
172
|
+
var who = me.builder || me;
|
|
173
|
+
// No id from /me marks no row as yours, rather than the wrong one.
|
|
174
|
+
viewerId = who && who.id != null ? who.id : null;
|
|
175
|
+
return client.request('GET', '/api/bongos/builders/roster').then(function (roster) {
|
|
176
|
+
if (!roster || !roster.ok || !roster.data || !Array.isArray(roster.data.builders)) return null;
|
|
177
|
+
return roster.data.builders;
|
|
178
|
+
});
|
|
179
|
+
}).catch(function () { return null; });
|
|
180
|
+
}
|
|
181
|
+
function applyGov(rows) {
|
|
182
|
+
gov = {};
|
|
183
|
+
rows.forEach(function (r) { if (r.id != null) gov[String(r.id)] = r; });
|
|
184
|
+
// The rank a control just changed reads from the fresh row.
|
|
185
|
+
all.forEach(function (b) { var g = govOf(b); if (g && g.rank) b.rank = g.rank; });
|
|
186
|
+
}
|
|
187
|
+
function claimsHtml(g) {
|
|
188
|
+
var stale = g.stale_claim ? ' <span class="badge badge--sev-medium">stale</span>' : '';
|
|
189
|
+
return '<td class="ledger__num' + (g.stale_claim ? ' roster-warn' : '') + '">' + esc(fmtInt(g.active_claim_count || 0)) + stale + '</td>';
|
|
190
|
+
}
|
|
191
|
+
function rankControlHtml(b) {
|
|
192
|
+
var opts = RANK_OPTIONS.slice();
|
|
193
|
+
if (b.rank && opts.indexOf(b.rank) === -1) opts.unshift(b.rank);
|
|
194
|
+
return '<select data-roster-rank data-id="' + esc(b.id) + '" data-current="' + esc(b.rank || '') + '" aria-label="Rank">'
|
|
195
|
+
+ opts.map(function (rk) {
|
|
196
|
+
return '<option value="' + esc(rk) + '"' + (rk === b.rank ? ' selected' : '') + '>' + esc(rankLabel(rk)) + '</option>';
|
|
197
|
+
}).join('') + '</select>';
|
|
198
|
+
}
|
|
199
|
+
// The viewer's own row offers nothing (task 1003720): both routes refuse a
|
|
200
|
+
// self-change outright, the guard that stops the last top-rank builder locking
|
|
201
|
+
// everyone out, so a live control there could only answer with a refusal.
|
|
202
|
+
function actionHtml(b, g) {
|
|
203
|
+
if (isViewer(b)) return '<span class="roster-self">That’s you</span>';
|
|
204
|
+
var inactive = String(g.status) === 'inactive';
|
|
205
|
+
return '<button type="button" class="btn-ghost' + (inactive ? '' : ' btn-ghost--danger') + '" data-roster-status="'
|
|
206
|
+
+ (inactive ? 'active' : 'inactive') + '" data-id="' + esc(b.id) + '">' + (inactive ? 'Reactivate' : 'Offboard') + '</button>';
|
|
207
|
+
}
|
|
208
|
+
function govFoot() {
|
|
209
|
+
var foot = document.getElementById('roster-gov-foot');
|
|
210
|
+
if (!foot) return;
|
|
211
|
+
if (!gov) { foot.hidden = true; return; }
|
|
212
|
+
foot.textContent = 'Because you hold a government seat you also see active claims and standing, and can change a builder’s rank. '
|
|
213
|
+
+ (all.some(isViewer)
|
|
214
|
+
? 'Your own row carries no controls. Nobody can change their own rank or standing — that guard is what stops the last '
|
|
215
|
+
+ rankLabel('archon') + ' locking everyone out — so another ' + rankLabel('archon') + ' does it.'
|
|
216
|
+
: '');
|
|
217
|
+
foot.hidden = false;
|
|
218
|
+
}
|
|
219
|
+
function patchBuilder(id, what, payload) {
|
|
220
|
+
return client.request('PATCH', '/api/bongos/builders/' + encodeURIComponent(id) + '/' + what, { body: payload })
|
|
221
|
+
.then(function (res) {
|
|
222
|
+
if (res && res.ok) return res.data;
|
|
223
|
+
var d = (res && res.data) || {};
|
|
224
|
+
var env = d.error && typeof d.error === 'object' ? d.error : null;
|
|
225
|
+
throw new Error((env && env.message) || d.message || (typeof d.error === 'string' ? d.error : '') || ('http ' + (res && res.status)));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function refreshGov() {
|
|
229
|
+
return loadGov().then(function (rows) { if (rows) applyGov(rows); else gov = null; makeRosterLedger(); rerender(); govFoot(); });
|
|
230
|
+
}
|
|
231
|
+
function wireGov() {
|
|
232
|
+
if (body['wired:gov']) return;
|
|
233
|
+
body['wired:gov'] = true;
|
|
234
|
+
// Delegated on the mount, so a page flip cannot strand a row without one.
|
|
235
|
+
body.addEventListener('change', function (e) {
|
|
236
|
+
var sel = e.target && e.target.closest ? e.target.closest('[data-roster-rank]') : null;
|
|
237
|
+
if (!sel) return;
|
|
238
|
+
var prev = sel.getAttribute('data-current');
|
|
239
|
+
var rank = sel.value;
|
|
240
|
+
if (rank === prev) return;
|
|
241
|
+
sel.disabled = true;
|
|
242
|
+
patchBuilder(sel.getAttribute('data-id'), 'rank', { rank: rank })
|
|
243
|
+
.then(function () { toast('Rank set to ' + rankLabel(rank) + '.'); return refreshGov(); })
|
|
244
|
+
.catch(function (err) { sel.value = prev; sel.disabled = false; toast('Could not change rank: ' + (err.message || err)); });
|
|
245
|
+
});
|
|
246
|
+
body.addEventListener('click', function (e) {
|
|
247
|
+
var btn = e.target && e.target.closest ? e.target.closest('[data-roster-status]') : null;
|
|
248
|
+
if (!btn) return;
|
|
249
|
+
var status = btn.getAttribute('data-roster-status');
|
|
250
|
+
var reason = status === 'inactive' ? (window.prompt('Reason for offboarding (optional):', '') || null) : null;
|
|
251
|
+
btn.disabled = true;
|
|
252
|
+
patchBuilder(btn.getAttribute('data-id'), 'status', { status: status, reason: reason })
|
|
253
|
+
.then(function () { toast(status === 'inactive' ? 'Builder offboarded.' : 'Builder reactivated.'); return refreshGov(); })
|
|
254
|
+
.catch(function (err) { btn.disabled = false; toast('Could not change standing: ' + (err.message || err)); });
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
139
258
|
// PLACE IS CANONICAL, not the row's position in the current sort.
|
|
140
259
|
// hall-render's leaderboardPlace() already defines a builder's place as
|
|
141
260
|
// credits-descending "independent of whatever column the table is currently
|
|
@@ -165,6 +284,12 @@
|
|
|
165
284
|
|
|
166
285
|
function matches(b) {
|
|
167
286
|
if (role && shownOf(b).indexOf(role) === -1) return false;
|
|
287
|
+
if (claims) {
|
|
288
|
+
var g = govOf(b);
|
|
289
|
+
if (!g) return false;
|
|
290
|
+
if (claims === 'stale' && !g.stale_claim) return false;
|
|
291
|
+
if (claims === 'holding' && !(Number(g.active_claim_count) > 0)) return false;
|
|
292
|
+
}
|
|
168
293
|
if (!query) return true;
|
|
169
294
|
var hay = ((b.github_login || '') + ' ' + (b.display_name || '')).toLowerCase();
|
|
170
295
|
return hay.indexOf(query) !== -1;
|
|
@@ -229,22 +354,31 @@
|
|
|
229
354
|
var tokens = hasTokens(b)
|
|
230
355
|
? esc(fmtInt(b.tokens_used))
|
|
231
356
|
: '<span class="lb-none" aria-label="No AI sessions">—</span>';
|
|
357
|
+
var g = govOf(b);
|
|
358
|
+
var govCells = g
|
|
359
|
+
? claimsHtml(g)
|
|
360
|
+
+ '<td><span class="badge badge--status">' + esc(g.status || '—') + '</span></td>'
|
|
361
|
+
+ '<td>' + actionHtml(b, g) + '</td>'
|
|
362
|
+
: (gov ? '<td class="ledger__num">—</td><td>—</td><td></td>' : '');
|
|
363
|
+
var rankCell = g && !isViewer(b) ? rankControlHtml(b) : rankBadgeHtml(b.rank);
|
|
232
364
|
return '<tr>'
|
|
233
365
|
+ '<td class="ledger__num ledger__place">' + esc(String(place || '—')) + idHtml(b) + '</td>'
|
|
234
366
|
+ '<td>' + whoHtml(b) + foundingHtml(b) + achRowHtml(b.achievements) + '</td>'
|
|
235
367
|
+ '<td class="ledger__roles">' + rolesHtml(b) + '</td>'
|
|
236
|
-
+ '<td class="ledger__rank">' +
|
|
368
|
+
+ '<td class="ledger__rank">' + rankCell + '</td>'
|
|
237
369
|
+ '<td class="ledger__num">' + esc(fmtInt(creditOf(b))) + '</td>'
|
|
238
370
|
+ '<td class="ledger__num">' + esc(fmtInt(b.karma)) + '</td>'
|
|
239
371
|
+ '<td class="ledger__num" title="' + esc(b.__eff || '') + '">' + tokens + '</td>'
|
|
372
|
+
+ govCells
|
|
240
373
|
+ '</tr>';
|
|
241
374
|
}
|
|
242
375
|
|
|
243
376
|
function columns() {
|
|
244
|
-
|
|
377
|
+
var cols = ['Place', 'Builder', 'Roles', 'Rank',
|
|
245
378
|
{ label: role ? currencyLabel() + ' as ' + CRAFT_WORD[role].toLowerCase() : currencyLabel(), class: 'ledger__num' },
|
|
246
379
|
{ label: 'Karma', class: 'ledger__num' },
|
|
247
380
|
{ label: 'Tokens', class: 'ledger__num' }];
|
|
381
|
+
return gov ? cols.concat([{ label: 'Active claims', class: 'ledger__num' }, 'Standing', 'Action']) : cols;
|
|
248
382
|
}
|
|
249
383
|
|
|
250
384
|
function makeRosterLedger() {
|
|
@@ -294,8 +428,21 @@
|
|
|
294
428
|
body.textContent = msg;
|
|
295
429
|
}
|
|
296
430
|
|
|
297
|
-
function mountRoster(rows) {
|
|
298
|
-
all =
|
|
431
|
+
function mountRoster(rows, govRows) {
|
|
432
|
+
all = Array.isArray(rows) ? rows : [];
|
|
433
|
+
if (govRows) {
|
|
434
|
+
// The government list is every builder; the public one only those who have
|
|
435
|
+
// earned. Nobody drops off the one list for having earned nothing yet.
|
|
436
|
+
var seen = {};
|
|
437
|
+
all.forEach(function (b) { if (b.id != null) seen[String(b.id)] = true; });
|
|
438
|
+
govRows.forEach(function (r) {
|
|
439
|
+
if (r.id == null || seen[String(r.id)]) return;
|
|
440
|
+
all.push({ id: r.id, github_login: r.github_login, display_name: r.display_name, avatar_url: r.avatar_url,
|
|
441
|
+
rank: r.rank, total_credits: r.total_credits, karma: r.karma, tokens_used: 0 });
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
all = stampPlaces(all);
|
|
445
|
+
if (govRows) applyGov(govRows);
|
|
299
446
|
if (!all.length) {
|
|
300
447
|
notice('Nobody has earned anything on this project yet. A builder joins the roster the '
|
|
301
448
|
+ 'first time they ship a claimed task.');
|
|
@@ -307,10 +454,19 @@
|
|
|
307
454
|
+ ' on this project, ranked by ' + currencyLabel().toLowerCase() + ' earned. This '
|
|
308
455
|
+ 'project’s own ledger — everyone who builds here is listed.';
|
|
309
456
|
}
|
|
457
|
+
// Watch's stale-claims tile links here as ?claims=stale.
|
|
458
|
+
var asked = '';
|
|
459
|
+
try { asked = new URLSearchParams(window.location.search).get('claims') || ''; } catch (e) { asked = ''; }
|
|
460
|
+
claims = gov && (asked === 'stale' || asked === 'holding') ? asked : '';
|
|
461
|
+
var claimsControl = gov ? [{ id: 'roster-claims', label: 'Claims', value: claims, options: [
|
|
462
|
+
{ value: '', label: 'anyone' },
|
|
463
|
+
{ value: 'holding', label: 'holding a claim' },
|
|
464
|
+
{ value: 'stale', label: 'stale claims' },
|
|
465
|
+
] }] : [];
|
|
310
466
|
kit.makeFilterBar({
|
|
311
467
|
mount: document.getElementById('roster-filters'),
|
|
312
468
|
search: { id: 'roster-q', label: 'Search', placeholder: 'name or login' },
|
|
313
|
-
primary: [
|
|
469
|
+
primary: claimsControl.concat([
|
|
314
470
|
{ id: 'roster-sort',
|
|
315
471
|
label: 'Sort',
|
|
316
472
|
options: [
|
|
@@ -319,10 +475,11 @@
|
|
|
319
475
|
{ value: 'karma', label: 'most karma' },
|
|
320
476
|
{ value: 'tokens', label: 'most tokens' },
|
|
321
477
|
] },
|
|
322
|
-
],
|
|
478
|
+
]),
|
|
323
479
|
onChange: function (vals) {
|
|
324
480
|
query = String(vals['roster-q'] || '').trim().toLowerCase();
|
|
325
481
|
sortBy = String(vals['roster-sort'] || '');
|
|
482
|
+
if (gov) claims = String(vals['roster-claims'] || '');
|
|
326
483
|
pager.page = 1;
|
|
327
484
|
rerender();
|
|
328
485
|
},
|
|
@@ -330,21 +487,23 @@
|
|
|
330
487
|
mountRoleChips();
|
|
331
488
|
makeRosterLedger();
|
|
332
489
|
rerender();
|
|
490
|
+
if (gov) { wireGov(); govFoot(); }
|
|
333
491
|
}
|
|
334
492
|
|
|
335
493
|
var foundingLoad = window.OTBFounding ? window.OTBFounding.load() : Promise.resolve([]);
|
|
336
494
|
// The PUBLIC list (task 1004437): the same rows the authed /leaderboard
|
|
337
495
|
// carries plus each builder's roles, and the one the status page reads, so
|
|
338
496
|
// the hall and the public page cannot rank a role differently.
|
|
497
|
+
var govLoad = loadGov();
|
|
339
498
|
fetch('/api/bongos/public/leaderboard', { headers: { Accept: 'application/json' } })
|
|
340
499
|
.then(function (r) {
|
|
341
500
|
if (!r.ok) throw new Error('http ' + r.status);
|
|
342
501
|
return r.json();
|
|
343
502
|
})
|
|
344
503
|
.then(function (data) {
|
|
345
|
-
return foundingLoad.then(function (
|
|
346
|
-
marks = window.OTBFounding ? window.OTBFounding.byId(
|
|
347
|
-
mountRoster(data && data.leaderboard);
|
|
504
|
+
return Promise.all([foundingLoad, govLoad]).then(function (got) {
|
|
505
|
+
marks = window.OTBFounding ? window.OTBFounding.byId(got[0]) : {};
|
|
506
|
+
mountRoster(data && data.leaderboard, got[1]);
|
|
348
507
|
});
|
|
349
508
|
})
|
|
350
509
|
.catch(function () { notice('Couldn’t load the roster right now.'); });
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
|
|
62
62
|
// Every limit of the line rule, in characters unless named otherwise.
|
|
63
63
|
const LIMITS = Object.freeze({
|
|
64
|
-
nameWords:
|
|
64
|
+
nameWords: 14, name: 90, fact: 60, facts: 3, factLen: 48, actions: 2,
|
|
65
65
|
tiles: 3, tileValue: 8, tileWords: 2, tileLabel: 14,
|
|
66
66
|
status: 44, clearLine: 30, clearSub: 40, record: 4, recordLen: 30,
|
|
67
67
|
chips: 5, chip: 24, todayTarget: 10, pips: 12, saveLine: 48,
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
// /goals/rollups) for the districts, and the viewer's earnings for "your share
|
|
21
21
|
// this month". Where the real data had to bend the design, it is said once here
|
|
22
22
|
// and in the ship notes:
|
|
23
|
-
// * Districts
|
|
23
|
+
// * Districts (only with `districts: true`; the page draws none since the
|
|
24
|
+
// owner's 2026-11-04 direction, and centres the civic row). One per OPEN goal, in the order the goals come, between
|
|
24
25
|
// Automation and Review. Up to four keep the prototype's 90 px plots; past
|
|
25
26
|
// four each plot narrows (never under 56 px) and past that the street grows
|
|
26
27
|
// longer than the 1440 px artboard, so the page scrolls it sideways rather
|
|
@@ -330,9 +331,13 @@
|
|
|
330
331
|
// between districts, 18 otherwise), starting at 43. Past MAX_DISTRICTS open
|
|
331
332
|
// goals the first seven stand as districts and the eighth plot is the rest,
|
|
332
333
|
// staked out, a door to the goals page.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
// The street is the governance square (owner, 2026-11-04): the working areas
|
|
335
|
+
// are the city's own buildings, so by default no district stands on the
|
|
336
|
+
// street and the civic row is centred on the artboard. `districts` keeps the
|
|
337
|
+
// prototype's row (the kit's tests hold it to the prototype).
|
|
338
|
+
function layout(goals, districts) {
|
|
339
|
+
const shown = districts ? (goals.length > MAX_DISTRICTS ? goals.slice(0, MAX_DISTRICTS - 1) : goals) : [];
|
|
340
|
+
const more = districts ? goals.length - shown.length : 0;
|
|
336
341
|
const dw = districtWidth(shown.length + (more ? 1 : 0));
|
|
337
342
|
const segs = [];
|
|
338
343
|
SEGS.forEach((s) => {
|
|
@@ -346,6 +351,8 @@
|
|
|
346
351
|
if (i > 0) x += segs[i - 1].g !== s.g ? 30 : (s.d ? 10 : 18);
|
|
347
352
|
s.x = x; x += s.w;
|
|
348
353
|
});
|
|
354
|
+
const spare = ARTBOARD_W - (x + 43);
|
|
355
|
+
if (!districts && spare > 0) segs.forEach((s) => { s.x += Math.floor(spare / 2); });
|
|
349
356
|
return { segs: segs, width: Math.max(ARTBOARD_W, x + 43), districtW: dw };
|
|
350
357
|
}
|
|
351
358
|
|
|
@@ -510,6 +517,10 @@
|
|
|
510
517
|
// living GovLiving.livingModel() over the same feeds, or null (no Leads,
|
|
511
518
|
// no agents, no construction: the deep street with real data only)
|
|
512
519
|
// roster GET /builders/roster body, or null (the people reading)
|
|
520
|
+
// districts true: stand a district per open goal between Automation and
|
|
521
|
+
// Review (the prototype's row). The page leaves it off: the
|
|
522
|
+
// street is the governance square and the working areas are the
|
|
523
|
+
// city's (owner, 2026-11-04)
|
|
513
524
|
function streetModel(input) {
|
|
514
525
|
const opts = input || {};
|
|
515
526
|
const docket = opts.docket && typeof opts.docket === 'object' && Array.isArray(opts.docket.matters) ? opts.docket : null;
|
|
@@ -529,7 +540,7 @@
|
|
|
529
540
|
const goals = (Array.isArray(opts.goals) ? opts.goals : []).filter((g) => g && g.status === 'open' && g.id != null);
|
|
530
541
|
const byGoal = {};
|
|
531
542
|
(Array.isArray(opts.rollups) ? opts.rollups : []).forEach((r) => { if (r && r.goal_id != null) byGoal[String(r.goal_id)] = r; });
|
|
532
|
-
const L = layout(goals);
|
|
543
|
+
const L = layout(goals, opts.districts === true);
|
|
533
544
|
const segs = L.segs, W = L.width;
|
|
534
545
|
const at = {};
|
|
535
546
|
segs.forEach((s) => { at[s.k || ('d:' + s.d)] = s; });
|
|
@@ -725,7 +725,23 @@
|
|
|
725
725
|
return dv;
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
// The floor picker (owner, 2026-11-04: a building is a place to work). On a
|
|
729
|
+
// wide window the section is cropped to the building itself (PICKER_CROP: its
|
|
730
|
+
// crown, storeys and ground, without the 800 x 708 section's empty margins)
|
|
731
|
+
// and scaled to the stage height, never under half size (a floor stays a
|
|
732
|
+
// 24 px target) nor over 0.72 (the desk keeps the room). The CSS variables
|
|
733
|
+
// street.js sets on the drawing's box and its column.
|
|
734
|
+
const PICKER_CROP = Object.freeze({ x: 50, y: 120, w: 590, h: 580 });
|
|
735
|
+
function pickerFit(stageH) {
|
|
736
|
+
const c = PICKER_CROP;
|
|
737
|
+
const k = Math.max(0.5, Math.min(0.72, Math.round(((Number(stageH) || 708) - 60) / c.h * 1000) / 1000));
|
|
738
|
+
const px = (n) => Math.round(n * k) + 'px';
|
|
739
|
+
return { k: k, box: { '--inside-art-scale': String(k), '--inside-art-x': px(-c.x), '--inside-art-y': px(-c.y) },
|
|
740
|
+
art: { '--inside-art-w': px(c.w), '--inside-art-h': px(c.h) } };
|
|
741
|
+
}
|
|
742
|
+
|
|
728
743
|
const api = {
|
|
744
|
+
pickerFit: pickerFit, PICKER_CROP: PICKER_CROP,
|
|
729
745
|
crown: crown, workItem: workItem, floorSymbol: floorSymbol, symbolPath: symbolPath, buildingView: buildingView, districtView: districtView,
|
|
730
746
|
civicInfo: civicInfo, floorFor: floorFor, holderOf: holderOf, recordOf: recordOf, buildingModel: buildingModel,
|
|
731
747
|
districtInfo: districtInfo, districtModel: districtModel,
|
|
@@ -530,6 +530,37 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
530
530
|
.inside-name:focus { outline: none; }
|
|
531
531
|
.inside-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
|
|
532
532
|
|
|
533
|
+
/* THE HALL IS FOR WORK (owner, 2026-11-04; task 1005075). The street's own
|
|
534
|
+
header strip and the six readings along the bottom repeat what the shell's
|
|
535
|
+
top bar and the buildings already say, and inside a building they squeezed
|
|
536
|
+
the floor you came to work on into a corner. Both go. */
|
|
537
|
+
.street-top, .street-signs { display: none; }
|
|
538
|
+
|
|
539
|
+
/* Inside a building: the drawing is the floor picker, at half size in its own
|
|
540
|
+
column (a floor stays a 24 px target); the chosen floor's desk takes the rest
|
|
541
|
+
of the window and scrolls on its own. The building's own facts (who holds it,
|
|
542
|
+
how it decides, its record) follow the desk instead of standing above it. */
|
|
543
|
+
@media (min-width: 900px) {
|
|
544
|
+
.inside--work { display: block; overflow: hidden; }
|
|
545
|
+
.inside--work .inside-sizer { width: auto; height: 100%; margin: 0; overflow: visible; }
|
|
546
|
+
.inside--work .inside-board { position: static; width: auto; height: 100%; transform: none; display: grid;
|
|
547
|
+
grid-template-columns: var(--inside-art-w, 400px) minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
|
|
548
|
+
column-gap: 40px; padding: 0 32px 0 16px; box-sizing: border-box; animation: none; }
|
|
549
|
+
.inside--work .inside-head { position: static; grid-column: 1 / -1; padding: 14px 0 6px 16px; }
|
|
550
|
+
.inside--work .inside-art { position: relative; width: var(--inside-art-w, 400px); height: var(--inside-art-h, 354px); grid-row: 2; align-self: start; overflow: hidden; }
|
|
551
|
+
.inside--work .inside-art__box { width: 800px; transform: translate(var(--inside-art-x, 0), var(--inside-art-y, 0)) scale(var(--inside-art-scale, 1)); }
|
|
552
|
+
.inside--work .inside-panel { position: static; grid-row: 2; width: auto; max-width: 880px; height: 100%; padding: 4px 4px 32px 0; box-sizing: border-box; gap: 20px; }
|
|
553
|
+
.inside--work .inside-title { order: 0; flex-direction: row; align-items: baseline; gap: 14px; flex-wrap: wrap; }
|
|
554
|
+
.inside--work .inside-name { font-size: 28px; }
|
|
555
|
+
.inside--work .inside-holds { font-size: 14px; }
|
|
556
|
+
.inside--work .inside-open { order: 1; }
|
|
557
|
+
.inside--work .govx { order: 2; }
|
|
558
|
+
.inside--work .inside-gov { order: 3; }
|
|
559
|
+
.inside--work .inside-record { order: 4; }
|
|
560
|
+
.inside--work .inside-open__head .inside-plate { width: 44px; height: 44px; border-radius: 10px; }
|
|
561
|
+
.inside--work .inside-open__title { font-size: 24px; }
|
|
562
|
+
}
|
|
563
|
+
|
|
533
564
|
/* narrow: the drawing in its own sideways strip, the panel below it */
|
|
534
565
|
@media (max-width: 899px) {
|
|
535
566
|
.inside { display: block; overflow-x: hidden; overflow-y: auto; }
|
|
@@ -543,6 +574,11 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
543
574
|
.inside-dhead { order: 1; }
|
|
544
575
|
.inside-legend { order: 3; flex-direction: column; gap: 8px; }
|
|
545
576
|
.inside-panel { order: 3; overflow: visible; }
|
|
577
|
+
/* the floor's desk straight under the name, the building's facts after it */
|
|
578
|
+
.inside-open { order: 1; }
|
|
579
|
+
.govx { order: 2; }
|
|
580
|
+
.inside-gov { order: 3; }
|
|
581
|
+
.inside-record { order: 4; }
|
|
546
582
|
.inside-name { font-size: 32px; }
|
|
547
583
|
.inside-gov__row { flex-wrap: wrap; row-gap: 4px; }
|
|
548
584
|
.desk-row__main { flex-wrap: wrap; row-gap: 8px; }
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
constitution: '/api/bongos/government/constitution',
|
|
78
78
|
uat: function (id) { return '/api/bongos/done-when/' + encodeURIComponent(id) + '/uat'; },
|
|
79
79
|
};
|
|
80
|
-
var NARROW = 900;
|
|
80
|
+
var NARROW = 900, FOCUS_W = 1120; // FOCUS_W: the civic row and its pins, which a wide window fits
|
|
81
81
|
var I = window.GovInside;
|
|
82
82
|
// The artboard's street band: the prototype's 900-high board less its 64 px
|
|
83
83
|
// header and 128 px footer, which this page lays out as real rows, and less
|
|
@@ -159,13 +159,10 @@
|
|
|
159
159
|
function here() { return S.parseHash(window.location && window.location.hash); }
|
|
160
160
|
|
|
161
161
|
// ---------- fit the artboard's street band to the stage ----------
|
|
162
|
-
// 1440 x 708 at scale 1 is the prototype exactly. A
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
// width) gives the spare height to the foot of the page, under the readings, so
|
|
167
|
-
// the street sits on them with no empty sky above it (task 1004718). The window
|
|
168
|
-
// gives the height back whenever a building's inside is open.
|
|
162
|
+
// 1440 x 708 at scale 1 is the prototype exactly. A wide window fits the civic
|
|
163
|
+
// row (FOCUS_W), crops the haze either side and centres the street in the stage
|
|
164
|
+
// (owner, 2026-11-04); a phone keeps it near full size and scrolls it sideways
|
|
165
|
+
// from the first building, so a pin stays a pin.
|
|
169
166
|
function fit() {
|
|
170
167
|
var win = $('streetWindow'), inside = $('streetInside');
|
|
171
168
|
var insideOn = !!(inside && !inside.hidden);
|
|
@@ -175,23 +172,29 @@
|
|
|
175
172
|
if (!win || !sizer || !board) return;
|
|
176
173
|
var W = view.model ? view.model.width : S.ARTBOARD_W;
|
|
177
174
|
var cw = win.clientWidth || window.innerWidth || 1440, ch = win.clientHeight || 708;
|
|
178
|
-
var
|
|
179
|
-
|
|
175
|
+
var wide = cw >= NARROW;
|
|
176
|
+
var s = wide ? Math.min(cw / FOCUS_W, ch / BAND_H) : Math.min(cw / W, ch / BAND_H);
|
|
177
|
+
if (cw / W < 0.6 && !wide) s = Math.min(1, ch / BAND_H);
|
|
180
178
|
s = Math.max(0.4, Math.round(s * 1000) / 1000);
|
|
181
179
|
view.scale = s;
|
|
182
|
-
|
|
180
|
+
var sizerW = wide ? Math.min(Math.round(W * s), cw) : Math.round(W * s);
|
|
181
|
+
sizer.style.width = sizerW + 'px';
|
|
183
182
|
sizer.style.height = Math.round(BAND_H * s) + 'px';
|
|
184
|
-
|
|
183
|
+
board.style.left = wide ? Math.round((sizerW - W * s) / 2) + 'px' : '';
|
|
184
|
+
if (win.style) win.style.overflowX = wide ? 'hidden' : '';
|
|
185
185
|
board.style.width = W + 'px';
|
|
186
186
|
board.style.setProperty('--street-scale', String(s));
|
|
187
187
|
var svg = $('streetSvg');
|
|
188
188
|
if (svg) { svg.setAttribute('width', String(W)); svg.setAttribute('viewBox', '0 0 ' + W + ' 900'); }
|
|
189
|
+
var doors = $('streetDoors'), first = doors && (doors.firstElementChild || (doors.children && doors.children[0]));
|
|
190
|
+
if (!wide && !insideOn && !view.scrolled && first) { win.scrollLeft = Math.max(0, Math.round((parseFloat(first.style.left) || 0) * s) - 16); view.scrolled = true; }
|
|
189
191
|
placeCard();
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
// The interior
|
|
193
|
-
//
|
|
194
|
-
//
|
|
194
|
+
// The interior: a building on a wide window is laid out, its drawing the floor
|
|
195
|
+
// picker (GovInside.pickerFit) beside the desk; a district's 1440 x 708 board
|
|
196
|
+
// scales to the window like the street; under NARROW px the board is a column
|
|
197
|
+
// and only the drawing scales, in its own sideways strip, never under 0.6.
|
|
195
198
|
function fitInside() {
|
|
196
199
|
var box = $('streetInside'), sizer = $('insideSizer'), board = $('insideBoard'), art = $('insideArt'), artBox = $('insideArtBox'), span = $('insideArtSpan');
|
|
197
200
|
if (!box || !sizer || !board || !art || !artBox || box.hidden) return;
|
|
@@ -207,6 +210,14 @@
|
|
|
207
210
|
if (span) { span.style.width = Math.round(artW * a) + 'px'; span.style.height = Math.round(708 * a) + 'px'; }
|
|
208
211
|
return;
|
|
209
212
|
}
|
|
213
|
+
var work = !(view.inside && view.inside.kind === 'district');
|
|
214
|
+
if (box.classList) box.classList.toggle('inside--work', work);
|
|
215
|
+
if (work) {
|
|
216
|
+
var f = I.pickerFit(ch);
|
|
217
|
+
sizer.style.width = ''; sizer.style.height = ''; board.style.setProperty('--inside-scale', '1');
|
|
218
|
+
[[artBox, f.box], [art, f.art]].forEach(function (e) { Object.keys(e[1]).forEach(function (k) { e[0].style.setProperty(k, e[1][k]); }); });
|
|
219
|
+
if (span) { span.style.width = '0px'; span.style.height = '0px'; } return;
|
|
220
|
+
}
|
|
210
221
|
var s = Math.max(0.4, Math.round(Math.min(cw / 1440, ch / 708) * 1000) / 1000);
|
|
211
222
|
sizer.style.width = Math.round(1440 * s) + 'px';
|
|
212
223
|
sizer.style.height = Math.round(708 * s) + 'px';
|
|
@@ -292,7 +303,9 @@
|
|
|
292
303
|
// GovStreetDepth.page's (street-depth.js); this file hands it the street's
|
|
293
304
|
// data, its card opener and a redraw, and keeps the street's own wiring.
|
|
294
305
|
var X = window.GovStreetDepth.page;
|
|
295
|
-
|
|
306
|
+
// a pin goes into its building, the matter a desk row there (owner, 2026-11-04: no pop-ups)
|
|
307
|
+
function goPin(key, el, keys) { var b = el && el.getAttribute && el.getAttribute('data-building'); if (b) window.location.hash = 'building=' + b; else openCard(key, el, keys); }
|
|
308
|
+
function drawPin(p) { return X.pin(p, { open: view.open, openCard: goPin }); }
|
|
296
309
|
function drawSign(sg, tile) { return X.sign(sg, tile); }
|
|
297
310
|
function drawDepth(dm, W) { return X.depth(dm, W); }
|
|
298
311
|
// the living model arrives after the street's own reads: the street and the deck draw it
|
|
@@ -21,12 +21,6 @@
|
|
|
21
21
|
/* Row actions inside a ledger cell: ghost pills in a row. */
|
|
22
22
|
.watch-actions { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
23
23
|
|
|
24
|
-
/* The reader's own roster row (task 1003720). Nobody may change their own rank
|
|
25
|
-
or standing — the server refuses both outright — so that row states a fact
|
|
26
|
-
where the others carry a control, and a fact is written in the faint ink, not
|
|
27
|
-
painted as a disabled pill. The foot line under the table says why. */
|
|
28
|
-
.watch-self { color: var(--ink-faint); }
|
|
29
|
-
|
|
30
24
|
/* A sealed or failed section says so in place. Three states, three sentences:
|
|
31
25
|
"nothing here" is the kit's empty state, this is "you may not see this" and
|
|
32
26
|
"could not load". */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Watch — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="Archon oversight for {{worldName}} — access requests, security,
|
|
7
|
+
<meta name="description" content="Archon oversight for {{worldName}} — access requests, security, stale claims, reviews, and repo health.">
|
|
8
8
|
<!-- The hub's origin, for the link from a guild's head in the applications queue to
|
|
9
9
|
its page on the hub (task 1002304). Empty on a vanilla instance: no link. -->
|
|
10
10
|
<meta name="hub-origin" content="{{hubOrigin}}">
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<div class="stat-row ov-summary" id="watch-attention" hidden>
|
|
49
49
|
<a class="stat-tile" href="#people"><span class="stat-tile__label">Access requests</span><span class="stat-tile__num" id="att-access">—</span><span class="stat-tile__sub">pending, on the People tab</span></a>
|
|
50
50
|
<a class="stat-tile" href="#security"><span class="stat-tile__label">Security reports</span><span class="stat-tile__num" id="att-security">—</span><span class="stat-tile__sub">open, on the Security tab</span></a>
|
|
51
|
-
<a class="stat-tile" href="
|
|
51
|
+
<a class="stat-tile" href="/roster?claims=stale"><span class="stat-tile__label">Stale claims</span><span class="stat-tile__num" id="att-stale">—</span><span class="stat-tile__sub">builders, on the Builders page</span></a>
|
|
52
52
|
<a class="stat-tile" href="#operations"><span class="stat-tile__label">Sessions</span><span class="stat-tile__num" id="att-sessions">—</span><span class="stat-tile__sub">recorded, on the Operations tab</span></a>
|
|
53
53
|
</div>
|
|
54
54
|
|
|
@@ -66,13 +66,6 @@
|
|
|
66
66
|
<div id="gate-body"><span class="kit-skel"></span></div>
|
|
67
67
|
</section>
|
|
68
68
|
|
|
69
|
-
<!-- 2. ROSTER -->
|
|
70
|
-
<section class="scroll" id="roster-scroll" aria-labelledby="roster-h" hidden>
|
|
71
|
-
<h2 id="roster-h" class="scroll__h">Roster</h2>
|
|
72
|
-
<p class="scroll__lede">Every builder in {{worldName}} — rank, {{currency}}, karma, and active claims.</p>
|
|
73
|
-
<div id="roster-body"><span class="kit-skel"></span></div>
|
|
74
|
-
</section>
|
|
75
|
-
|
|
76
69
|
<!-- 3. REVIEWS BY BUILDER -->
|
|
77
70
|
<section class="scroll" id="marks-scroll" aria-labelledby="marks-h" hidden>
|
|
78
71
|
<h2 id="marks-h" class="scroll__h">Reviews by builder</h2>
|
|
@@ -175,6 +168,6 @@
|
|
|
175
168
|
<script src="/builders/hall-kit.js"></script>
|
|
176
169
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
|
|
177
170
|
<script src="/builders/brand-apply.js?v=2026-06-19-r56"></script>
|
|
178
|
-
<script src="/builders/watch.js?v=2026-
|
|
171
|
+
<script src="/builders/watch.js?v=2026-11-05-roster"></script>
|
|
179
172
|
</body>
|
|
180
173
|
</html>
|