@bongos/core 1.19.457 → 1.19.459
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 +126 -46
- package/.claude/skills/design/SKILL.md +10 -0
- package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +30 -0
- package/docs/adr/README.md +1 -0
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +160 -177
- package/docs/copy-registry.json +445 -606
- package/docs/file-map.md +2 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/ui-look-before-you-ship.md +2 -0
- package/modules/hall-ui/public/gate.css +20 -88
- package/modules/hall-ui/public/gate.html +24 -12
- package/modules/hall-ui/public/gate.js +77 -60
- package/modules/hall-ui/public/gate.states.json +19 -1
- package/modules/hall-ui/public/government.css +96 -154
- package/modules/hall-ui/public/government.html +80 -52
- package/modules/hall-ui/public/government.js +227 -136
- package/modules/hall-ui/public/government.states.json +71 -1
- package/modules/hall-ui/public/harbor.css +9 -120
- package/modules/hall-ui/public/harbor.html +23 -11
- package/modules/hall-ui/public/harbor.js +109 -83
- package/modules/hall-ui/public/harbor.states.json +19 -1
- package/modules/hall-ui/public/oversight.css +296 -0
- package/modules/hall-ui/public/watch.css +56 -170
- package/modules/hall-ui/public/watch.html +48 -37
- package/modules/hall-ui/public/watch.js +363 -298
- package/modules/hall-ui/public/watch.states.json +48 -1
- package/modules/ui-design/kit/README.md +7 -5
- package/modules/ui-design/kit/check-mock.js +12 -5
- package/modules/ui-design/kit/contrast.js +13 -6
- package/modules/ui-design/kit/lib.js +39 -2
- package/modules/ui-design/kit/probe.js +4 -1
- package/modules/ui-design/kit/render.js +7 -4
- package/modules/ui-design/kit/serve.js +9 -2
- package/modules/ui-design/styles/README.md +54 -2
- package/modules/ui-design/styles/chrome-world/DESIGN.md +56 -0
- package/modules/ui-design/styles/chrome-world/mock.html +275 -0
- package/modules/ui-design/styles/chrome-world/mock.states.json +11 -0
- package/modules/ui-design/styles/chrome-world/pack.json +33 -0
- package/modules/ui-design/styles/expedition/DESIGN.md +141 -0
- package/modules/ui-design/styles/expedition/mock.html +285 -0
- package/modules/ui-design/styles/expedition/mock.states.json +11 -0
- package/modules/ui-design/styles/expedition/pack.json +33 -0
- package/modules/ui-design/styles/grove/DESIGN.md +148 -0
- package/modules/ui-design/styles/grove/mock.html +294 -0
- package/modules/ui-design/styles/grove/mock.states.json +11 -0
- package/modules/ui-design/styles/grove/pack.json +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/cosmos_token_contrast.mjs +9 -2
- package/tests/hall_oversight_world.mjs +118 -0
- package/tests/hall_tokens.mjs +15 -6
- package/tests/ui_design_styles.mjs +218 -0
- package/tests/world_tokens.mjs +9 -2
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
// What simplifying may NOT touch is ADR 0175 §8: an objection still carries a
|
|
59
59
|
// reason and names one section. That requirement is why voting lives in the
|
|
60
60
|
// hall and not on a chat reaction.
|
|
61
|
+
//
|
|
62
|
+
// TASK 1003469 put the page in the chrome world's COMPOSITION, not just its
|
|
63
|
+
// tokens: every section is a shell .scroll panel with the display-voice heading,
|
|
64
|
+
// every repeated thing is the oversight family's .ov-row (oversight.css), the
|
|
65
|
+
// five unrelated lists that shared `.gov-assign-row` each render as the row they
|
|
66
|
+
// are, the permission grid's bordered boxes became hairline groups, and a rank's
|
|
67
|
+
// DESCRIPTION — collected by the create form and never shown — is the row's sub
|
|
68
|
+
// line. Nothing about the data, the locks, the tab ids or the ballot changed.
|
|
61
69
|
|
|
62
70
|
(() => {
|
|
63
71
|
'use strict';
|
|
@@ -67,7 +75,8 @@
|
|
|
67
75
|
// /lib/bongos-client.js. baseUrl:'' passes the full `${API}${path}` URLs below
|
|
68
76
|
// through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
|
|
69
77
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
70
|
-
const { escapeHtml } = window.OTB;
|
|
78
|
+
const { escapeHtml, fmtInt } = window.OTB;
|
|
79
|
+
const { emptyStateHtml } = window.OTBKit;
|
|
71
80
|
const toast = (msg) => window.OTB.toast(msg);
|
|
72
81
|
|
|
73
82
|
// ---- fetch helpers (mirror gate.js) --------------------------------------
|
|
@@ -104,15 +113,15 @@
|
|
|
104
113
|
bad_rank_key: 'Rank keys are kebab-case: lowercase letters, digits, dashes.',
|
|
105
114
|
validation_failed: 'Missing or invalid fields.',
|
|
106
115
|
// the vote refusals (R17 over R08's route)
|
|
107
|
-
not_board_member: 'Only members of this item
|
|
108
|
-
item_closed: 'This sitting has closed
|
|
109
|
-
vote_window_expired: 'This sitting
|
|
110
|
-
objection_needs_reason: 'An objection must say why
|
|
116
|
+
not_board_member: 'Only members of this item’s board may vote on it — membership follows the rules that were in force when the window opened.',
|
|
117
|
+
item_closed: 'This sitting has closed — refreshing.',
|
|
118
|
+
vote_window_expired: 'This sitting’s clock has run out — the sweep is closing it.',
|
|
119
|
+
objection_needs_reason: 'An objection must say why — write the reason.',
|
|
111
120
|
objection_needs_section: 'An objection must point at exactly one section of the idea.',
|
|
112
|
-
yes_carries_nothing: 'A yes carries no reason or section
|
|
113
|
-
bad_direction: 'A vote is yes or no
|
|
114
|
-
item_not_found: 'That item no longer exists
|
|
115
|
-
bad_item_id: 'That item id isn
|
|
121
|
+
yes_carries_nothing: 'A yes carries no reason or section — those belong to objections.',
|
|
122
|
+
bad_direction: 'A vote is yes or no — nothing else.',
|
|
123
|
+
item_not_found: 'That item no longer exists — refreshing.',
|
|
124
|
+
bad_item_id: 'That item id isn’t valid.',
|
|
116
125
|
};
|
|
117
126
|
const friendlyError = (code) => ERROR_COPY[code] || null;
|
|
118
127
|
|
|
@@ -126,26 +135,45 @@
|
|
|
126
135
|
|
|
127
136
|
const selectedRank = () => ranks.find((r) => r.rank_key === selectedRankKey) || null;
|
|
128
137
|
|
|
138
|
+
// ---- shared row pieces ---------------------------------------------------
|
|
139
|
+
|
|
140
|
+
// A state pill in the shell's vocabulary. `mod` is one of the .fact-pill
|
|
141
|
+
// states (ok / warn / danger / info / quiet); the label is what the reader sees.
|
|
142
|
+
const pill = (mod, label) => `<span class="fact-pill fact-pill--${mod}">${escapeHtml(label)}</span>`;
|
|
143
|
+
const groupHead = (name, n, note) =>
|
|
144
|
+
`<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(name)}</span>` +
|
|
145
|
+
(n === undefined ? '' : `<span class="ov-group__n">${fmtInt(n)}</span>`) +
|
|
146
|
+
(note ? `<span class="ov-group__note">${escapeHtml(note)}</span>` : '') +
|
|
147
|
+
'</div>';
|
|
148
|
+
// A timestamp the way the rows have always shown one: "2026-08-30 14:05".
|
|
149
|
+
const when = (iso) => escapeHtml(String(iso || '').slice(0, 16).replace('T', ' '));
|
|
150
|
+
|
|
129
151
|
// ---- render: rank list ---------------------------------------------------
|
|
130
152
|
|
|
153
|
+
// One rank, one selectable oversight row: the label, the DESCRIPTION the
|
|
154
|
+
// create form collects (task 1003469 — it was written and never read back),
|
|
155
|
+
// then the kind pill, the key and the permission count as the facts line.
|
|
131
156
|
function renderRanks() {
|
|
132
157
|
const list = document.getElementById('gov-rank-list');
|
|
133
158
|
list.innerHTML = '';
|
|
134
159
|
for (const rank of ranks) {
|
|
135
160
|
// The Archon row is called out as read-only, because it is the one rank in
|
|
136
161
|
// this list a click will not change and the reader should know before trying.
|
|
137
|
-
const
|
|
138
|
-
?
|
|
139
|
-
: rank.is_system
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
const kind = isArchonRank(rank)
|
|
163
|
+
? pill('danger', 'ladder · read-only')
|
|
164
|
+
: rank.is_system ? pill('info', 'ladder') : pill('ok', 'custom');
|
|
165
|
+
const n = (rank.permissions || []).length;
|
|
166
|
+
const desc = String(rank.description || '').trim();
|
|
142
167
|
const btn = document.createElement('button');
|
|
143
168
|
btn.type = 'button';
|
|
144
|
-
btn.className = '
|
|
169
|
+
btn.className = 'ov-row';
|
|
145
170
|
btn.setAttribute('aria-pressed', rank.rank_key === selectedRankKey ? 'true' : 'false');
|
|
146
171
|
btn.innerHTML =
|
|
147
|
-
`<span class="
|
|
148
|
-
|
|
172
|
+
`<span class="ov-row__main">
|
|
173
|
+
<span class="ov-row__title">${escapeHtml(rank.label || rank.rank_key)}</span>
|
|
174
|
+
${desc ? `<span class="ov-row__sub">${escapeHtml(desc)}</span>` : ''}
|
|
175
|
+
<span class="ov-row__facts">${kind}<span class="ov-mono">${escapeHtml(rank.rank_key)}</span><span class="ov-fact"><b>${fmtInt(n)}</b> permission${n === 1 ? '' : 's'}</span></span>
|
|
176
|
+
</span>`;
|
|
149
177
|
btn.addEventListener('click', () => { selectedRankKey = rank.rank_key; renderRanks(); renderGrid(); });
|
|
150
178
|
list.appendChild(btn);
|
|
151
179
|
}
|
|
@@ -165,10 +193,13 @@
|
|
|
165
193
|
return !!p.grantable;
|
|
166
194
|
}
|
|
167
195
|
|
|
196
|
+
// One permission, one dense oversight row: the key as the title in the
|
|
197
|
+
// machine's face, the guard sentence as the sub line, the floor as a chip, and
|
|
198
|
+
// the control — a grant toggle, or the lock with its reason — on the right.
|
|
168
199
|
function permRow(p, rank) {
|
|
169
200
|
const held = !!rank && (rank.permissions || []).includes(p.key);
|
|
170
|
-
const row = document.createElement('
|
|
171
|
-
row.className = '
|
|
201
|
+
const row = document.createElement('li');
|
|
202
|
+
row.className = 'ov-row ov-row--dense';
|
|
172
203
|
|
|
173
204
|
// Why locked, in the words that tell the reader whether it is a policy they
|
|
174
205
|
// could change or a fact they cannot. Three causes, three sentences.
|
|
@@ -182,31 +213,35 @@
|
|
|
182
213
|
if (!rank) {
|
|
183
214
|
control = '';
|
|
184
215
|
} else if (!grantableTo(p, rank)) {
|
|
185
|
-
control = `<span class="
|
|
216
|
+
control = `<span class="fact-pill fact-pill--quiet" title="${escapeHtml(lockReason)}">locked</span>`;
|
|
186
217
|
// The Archon rank is read-only rather than empty-handed, so say what it holds.
|
|
187
218
|
if (held && isArchonRank(rank)) control = `<span class="gov-held-note">held</span>${control}`;
|
|
188
219
|
} else {
|
|
189
|
-
control = `<button type="button" class="
|
|
220
|
+
control = `<button type="button" class="ov-toggle" aria-pressed="${held ? 'true' : 'false'}" data-perm="${escapeHtml(p.key)}">${held ? 'held' : 'grant'}</button>`;
|
|
190
221
|
}
|
|
191
222
|
|
|
192
223
|
row.innerHTML =
|
|
193
|
-
`<div class="
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
<div class="
|
|
224
|
+
`<div class="ov-row__main">
|
|
225
|
+
<span class="gov-perm__key">${escapeHtml(p.key)}</span>
|
|
226
|
+
${p.guards ? `<p class="ov-row__sub">${escapeHtml(p.guards)}</p>` : ''}
|
|
227
|
+
<div class="ov-row__facts"><span class="fact-chip">${escapeHtml(p.floor || 'principal')}</span></div>
|
|
197
228
|
</div>
|
|
198
|
-
|
|
229
|
+
<div class="ov-row__right"><div class="ov-row__actions">${control}</div></div>`;
|
|
199
230
|
|
|
200
|
-
const btn = row.querySelector('button.
|
|
231
|
+
const btn = row.querySelector('button.ov-toggle');
|
|
201
232
|
if (btn) btn.addEventListener('click', () => toggleGrant(rank, p.key, !held, btn));
|
|
202
233
|
return row;
|
|
203
234
|
}
|
|
204
235
|
|
|
205
|
-
|
|
236
|
+
// A named run of permission rows inside the panel: the group head, then the rows.
|
|
237
|
+
function section(title, rows, note) {
|
|
206
238
|
const el = document.createElement('section');
|
|
207
|
-
el.className = '
|
|
208
|
-
el.innerHTML =
|
|
209
|
-
|
|
239
|
+
el.className = 'ov-group';
|
|
240
|
+
el.innerHTML = groupHead(title, rows.length, note);
|
|
241
|
+
const list = document.createElement('ul');
|
|
242
|
+
list.className = 'ov-rows';
|
|
243
|
+
for (const r of rows) list.appendChild(r);
|
|
244
|
+
el.appendChild(list);
|
|
210
245
|
return el;
|
|
211
246
|
}
|
|
212
247
|
|
|
@@ -225,7 +260,7 @@
|
|
|
225
260
|
const grid = document.getElementById('gov-grid');
|
|
226
261
|
const rank = selectedRank();
|
|
227
262
|
document.getElementById('gov-grid-title').textContent = rank
|
|
228
|
-
? `Permissions
|
|
263
|
+
? `Permissions for ${rank.label || rank.rank_key}` : 'Permissions';
|
|
229
264
|
document.getElementById('gov-grid-note').textContent = gridNote(rank);
|
|
230
265
|
|
|
231
266
|
// The reset escape hatch — only a ladder rank has a catalog default, and the
|
|
@@ -235,7 +270,7 @@
|
|
|
235
270
|
if (isRankRank(rank) && !isArchonRank(rank)) {
|
|
236
271
|
const btn = document.createElement('button');
|
|
237
272
|
btn.type = 'button';
|
|
238
|
-
btn.className = '
|
|
273
|
+
btn.className = 'btn-ghost';
|
|
239
274
|
btn.textContent = 'Reset to default';
|
|
240
275
|
btn.addEventListener('click', () => resetRank(rank, btn));
|
|
241
276
|
resetBox.appendChild(btn);
|
|
@@ -251,13 +286,11 @@
|
|
|
251
286
|
const ownerPowers = humanPerms.filter((p) => p.system && !p.substrate);
|
|
252
287
|
const operational = humanPerms.filter((p) => !p.system);
|
|
253
288
|
|
|
254
|
-
const
|
|
255
|
-
? `Owner powers (${ownerPowers.length})`
|
|
256
|
-
: `Owner powers — locked for custom ranks (${ownerPowers.length})`;
|
|
289
|
+
const ownerNote = !rank || isRankRank(rank) ? '' : 'locked for custom ranks';
|
|
257
290
|
|
|
258
|
-
grid.appendChild(section(
|
|
259
|
-
grid.appendChild(section(
|
|
260
|
-
grid.appendChild(section(
|
|
291
|
+
grid.appendChild(section('Operational', operational.map((p) => permRow(p, rank))));
|
|
292
|
+
grid.appendChild(section('Owner powers', ownerPowers.map((p) => permRow(p, rank)), ownerNote));
|
|
293
|
+
grid.appendChild(section('Enforced outside the app', substrate.map((p) => permRow(p, rank)), 'always locked'));
|
|
261
294
|
}
|
|
262
295
|
|
|
263
296
|
// Restore a ladder rank to the shipped ladder. Confirmed because it is the one
|
|
@@ -292,29 +325,36 @@
|
|
|
292
325
|
|
|
293
326
|
// ---- render: assignments -------------------------------------------------
|
|
294
327
|
|
|
328
|
+
// One assignment, one oversight row: the builder, then their id and the rank
|
|
329
|
+
// pill as facts, and Remove on the right for a custom rank.
|
|
295
330
|
function renderAssignments() {
|
|
296
331
|
const box = document.getElementById('gov-assignments');
|
|
297
332
|
box.innerHTML = '';
|
|
298
333
|
if (assignments.length === 0) {
|
|
299
|
-
box.innerHTML =
|
|
334
|
+
box.innerHTML = emptyStateHtml('No rank assignments yet.');
|
|
300
335
|
return;
|
|
301
336
|
}
|
|
302
|
-
const
|
|
303
|
-
|
|
337
|
+
const list = document.createElement('ul');
|
|
338
|
+
list.className = 'ov-rows';
|
|
304
339
|
for (const a of assignments) {
|
|
305
|
-
const row = document.createElement('
|
|
306
|
-
row.className = '
|
|
307
|
-
const who =
|
|
340
|
+
const row = document.createElement('li');
|
|
341
|
+
row.className = 'ov-row';
|
|
342
|
+
const who = escapeHtml(a.display_name || a.github_login || `builder ${a.builder_id}`);
|
|
308
343
|
const rankPill = a.is_system
|
|
309
|
-
?
|
|
310
|
-
:
|
|
311
|
-
const action = a.is_system ? '' :
|
|
312
|
-
row.innerHTML =
|
|
344
|
+
? pill('info', `${a.rank_label || a.rank_key} · via rank`)
|
|
345
|
+
: pill('ok', a.rank_label || a.rank_key);
|
|
346
|
+
const action = a.is_system ? '' : '<button type="button" class="btn-ghost" data-act="unassign">Remove</button>';
|
|
347
|
+
row.innerHTML =
|
|
348
|
+
`<div class="ov-row__main">
|
|
349
|
+
<span class="ov-row__title">${who}</span>
|
|
350
|
+
<div class="ov-row__facts">${rankPill}<span class="ov-mono">#${escapeHtml(String(a.builder_id))}</span>${a.rank ? `<span class="ov-fact">rank <b>${escapeHtml(window.OTB.rankLabel(a.rank))}</b></span>` : ''}</div>
|
|
351
|
+
</div>
|
|
352
|
+
<div class="ov-row__right"><div class="ov-row__actions">${action}</div></div>`;
|
|
313
353
|
const btn = row.querySelector('[data-act=unassign]');
|
|
314
354
|
if (btn) btn.addEventListener('click', () => unassign(a, btn));
|
|
315
|
-
|
|
355
|
+
list.appendChild(row);
|
|
316
356
|
}
|
|
317
|
-
box.appendChild(
|
|
357
|
+
box.appendChild(list);
|
|
318
358
|
}
|
|
319
359
|
|
|
320
360
|
function renderAssignSelect() {
|
|
@@ -322,7 +362,7 @@
|
|
|
322
362
|
const custom = ranks.filter((r) => !r.is_system);
|
|
323
363
|
sel.innerHTML = custom.length
|
|
324
364
|
? custom.map((r) => `<option value="${escapeHtml(r.rank_key)}">${escapeHtml(r.label || r.rank_key)}</option>`).join('')
|
|
325
|
-
:
|
|
365
|
+
: '<option value="" disabled selected>No custom ranks yet</option>';
|
|
326
366
|
}
|
|
327
367
|
|
|
328
368
|
async function unassign(a, btn) {
|
|
@@ -340,22 +380,29 @@
|
|
|
340
380
|
|
|
341
381
|
// ---- render: principals --------------------------------------------------
|
|
342
382
|
|
|
383
|
+
// One machine identity, one dense row: the identity as the title, what it holds
|
|
384
|
+
// and under which guard as the sub line, and the lock as its one fact.
|
|
343
385
|
function renderPrincipals() {
|
|
344
386
|
const box = document.getElementById('gov-principals');
|
|
345
387
|
box.innerHTML = '';
|
|
346
388
|
if (principals.length === 0) {
|
|
347
|
-
box.innerHTML =
|
|
389
|
+
box.innerHTML = emptyStateHtml('No machine principals declared.');
|
|
348
390
|
return;
|
|
349
391
|
}
|
|
350
|
-
const
|
|
351
|
-
|
|
392
|
+
const list = document.createElement('ul');
|
|
393
|
+
list.className = 'ov-rows';
|
|
352
394
|
for (const p of principals) {
|
|
353
|
-
const row = document.createElement('
|
|
354
|
-
row.className = '
|
|
355
|
-
row.innerHTML =
|
|
356
|
-
|
|
395
|
+
const row = document.createElement('li');
|
|
396
|
+
row.className = 'ov-row ov-row--dense';
|
|
397
|
+
row.innerHTML =
|
|
398
|
+
`<div class="ov-row__main">
|
|
399
|
+
<span class="gov-perm__key">${escapeHtml(p.system_role)}</span>
|
|
400
|
+
<p class="ov-row__sub">holds <code>${escapeHtml(p.permission)}</code>${p.guards ? ` · ${escapeHtml(p.guards)}` : ''}</p>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="ov-row__right">${pill('quiet', 'locked')}</div>`;
|
|
403
|
+
list.appendChild(row);
|
|
357
404
|
}
|
|
358
|
-
box.appendChild(
|
|
405
|
+
box.appendChild(list);
|
|
359
406
|
}
|
|
360
407
|
|
|
361
408
|
// ---- loads ---------------------------------------------------------------
|
|
@@ -543,7 +590,7 @@
|
|
|
543
590
|
// the axis these three rules actually differ on and the one that surprises
|
|
544
591
|
// people: under the other two an unvoted item passes, under this one it does not.
|
|
545
592
|
if (board.pass_rule === 'majority') {
|
|
546
|
-
return 'Items pass by <strong>majority</strong>: more than half of the board must vote yes
|
|
593
|
+
return 'Items pass by <strong>majority</strong>: more than half of the board must vote yes — not just more yes than no. '
|
|
547
594
|
+ 'Silence is <em>not</em> agreement here, so an item that never reaches a majority is returned to its author. '
|
|
548
595
|
+ 'An author may vote on their own idea, but it can never be carried by their vote alone.';
|
|
549
596
|
}
|
|
@@ -560,21 +607,27 @@
|
|
|
560
607
|
return `A sitting runs <strong>${fmtWindow(board.window_minutes)}</strong>${early}. An unvoted window nulls that member's vote — absence never blocks.`;
|
|
561
608
|
}
|
|
562
609
|
|
|
610
|
+
// One dial, one row: the dial's name as the title, the live sentence as the
|
|
611
|
+
// sub line. The sentence is trusted HTML from the helpers above.
|
|
612
|
+
function dialRow(name, sentenceHtml) {
|
|
613
|
+
return `<li class="ov-row"><div class="ov-row__main"><span class="ov-row__title">${escapeHtml(name)}</span><p class="ov-row__sub">${sentenceHtml}</p></div></li>`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// One closed amendment sitting, one row: who proposed what, the rationale as
|
|
617
|
+
// the sub line, the outcome pill and the date and rule as facts.
|
|
563
618
|
function historyRow(h) {
|
|
564
619
|
const who = escapeHtml(h.proposer_name || h.proposer_login || (h.proposed_by ? `builder ${h.proposed_by}` : 'unknown'));
|
|
565
|
-
const
|
|
566
|
-
const pill = h.outcome === 'passed'
|
|
567
|
-
? '<span class="gov-pill gov-pill--seeded">ratified</span>'
|
|
568
|
-
: '<span class="gov-pill gov-pill--locked">returned</span>';
|
|
620
|
+
const outcome = h.outcome === 'passed' ? pill('ok', 'ratified') : pill('warn', 'returned');
|
|
569
621
|
// Task 1003270: the rule by name, not by config key. An unknown value still
|
|
570
622
|
// shows verbatim — a rule this build does not know about is worth seeing.
|
|
571
623
|
const under = h.decided_under && h.decided_under.pass_rule
|
|
572
|
-
?
|
|
624
|
+
? `<span class="ov-fact">decided ${escapeHtml(PASS_RULE_NAME[h.decided_under.pass_rule] || `under ${h.decided_under.pass_rule}`)}</span>`
|
|
573
625
|
: '';
|
|
574
|
-
return `<
|
|
575
|
-
<span class="
|
|
576
|
-
${
|
|
577
|
-
|
|
626
|
+
return `<li class="ov-row"><div class="ov-row__main">
|
|
627
|
+
<span class="ov-row__title">${who} proposed <code>${escapeHtml(JSON.stringify(h.proposed))}</code></span>
|
|
628
|
+
${h.rationale_md ? `<p class="ov-row__sub">${escapeHtml(h.rationale_md)}</p>` : ''}
|
|
629
|
+
<div class="ov-row__facts">${outcome}<span class="ov-row__when">${when(h.closed_at)}</span>${under}</div>
|
|
630
|
+
</div></li>`;
|
|
578
631
|
}
|
|
579
632
|
|
|
580
633
|
// ---- the charter library (R19) ---------------------------------------------
|
|
@@ -643,52 +696,75 @@
|
|
|
643
696
|
];
|
|
644
697
|
const LADDER = 'solo/pair → circle (3–8) → ~9 name an executive → ~25 → ~50 written charter + council → ~150 adjudication separates → beyond: federate';
|
|
645
698
|
|
|
699
|
+
// One charter, one row: the name, the blurb as the sub line, whether it is
|
|
700
|
+
// selectable and recommended as pills, and the configuration an owner would
|
|
701
|
+
// type as the machine-voiced line under it.
|
|
646
702
|
function charterCard(c, recommendedKey) {
|
|
647
|
-
const
|
|
648
|
-
? '
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const how = c.how ? `<
|
|
652
|
-
return `<
|
|
703
|
+
const facts = [
|
|
704
|
+
c.selectable ? pill('ok', 'selectable today') : pill('quiet', 'documented for later'),
|
|
705
|
+
c.key === recommendedKey ? pill('info', 'recommended at your headcount') : '',
|
|
706
|
+
].join('');
|
|
707
|
+
const how = c.how ? `<p class="gov-charter__how">as configuration: ${escapeHtml(c.how)}</p>` : '';
|
|
708
|
+
return `<li class="ov-row"><div class="ov-row__main">
|
|
709
|
+
<span class="ov-row__title">${escapeHtml(c.name)}</span>
|
|
710
|
+
<p class="ov-row__sub">${escapeHtml(c.blurb)}</p>
|
|
711
|
+
<div class="ov-row__facts">${facts}</div>
|
|
712
|
+
${how}
|
|
713
|
+
</div></li>`;
|
|
653
714
|
}
|
|
654
715
|
|
|
716
|
+
// The library and the trajectories as one panel: the seven structures, then
|
|
717
|
+
// the three trajectories under their own group head, then the ladder as the
|
|
718
|
+
// panel's foot line.
|
|
655
719
|
function charterLibraryHtml(data) {
|
|
656
720
|
const rec = data.recommended || {};
|
|
657
721
|
return `
|
|
658
|
-
<
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
722
|
+
<section class="scroll" aria-labelledby="gov-charters-h">
|
|
723
|
+
<h2 class="scroll__h" id="gov-charters-h">Charter library</h2>
|
|
724
|
+
<p class="scroll__lede">The seven government structures, for getting a team up to speed before deciding when to flip a switch. <strong>Selectable today</strong> means expressible through the real configuration — a rank-based membership plus a pass rule — with no new code. This project has <strong>${escapeHtml(String(data.headcount ?? '?'))}</strong> active builder${data.headcount === 1 ? '' : 's'}: ${escapeHtml(rec.why || '')} <em>(${escapeHtml(rec.rung || '')})</em>.</p>
|
|
725
|
+
<ul class="ov-rows">${CHARTERS.map((c) => charterCard(c, rec.key)).join('')}</ul>
|
|
726
|
+
<section class="ov-group">
|
|
727
|
+
${groupHead('Trajectories', TRAJECTORIES.length, 'how a project moves between structures')}
|
|
728
|
+
<ul class="ov-rows">${TRAJECTORIES.map((t) => `<li class="ov-row"><div class="ov-row__main"><span class="ov-row__title">${escapeHtml(t.name)}</span><p class="ov-row__sub">${escapeHtml(t.blurb)}</p></div></li>`).join('')}</ul>
|
|
729
|
+
</section>
|
|
730
|
+
<p class="ov-foot">The developmental ladder: ${escapeHtml(LADDER)}. Formality is adopted only where informal coordination breaks.</p>
|
|
731
|
+
</section>
|
|
665
732
|
`;
|
|
666
733
|
}
|
|
667
734
|
|
|
668
735
|
async function loadConstitution() {
|
|
736
|
+
const live = document.getElementById('gov-constitution-live');
|
|
669
737
|
const box = document.getElementById('gov-constitution');
|
|
670
738
|
let data;
|
|
671
739
|
try {
|
|
672
740
|
data = await fetchJson('/government/constitution');
|
|
673
741
|
} catch (err) {
|
|
674
742
|
if (err && err.kind === '401') return;
|
|
675
|
-
|
|
743
|
+
live.innerHTML = emptyStateHtml('Could not load the constitution just now.');
|
|
744
|
+
box.innerHTML = '';
|
|
676
745
|
return;
|
|
677
746
|
}
|
|
678
747
|
const b = data.board || {};
|
|
679
748
|
const history = data.history || [];
|
|
749
|
+
// The three dials as rows inside the Constitution panel, then the
|
|
750
|
+
// configuration they resolve from as the panel's foot line.
|
|
751
|
+
live.innerHTML = `
|
|
752
|
+
<ul class="ov-rows">
|
|
753
|
+
${dialRow('Who sits on the board', membershipSentence(b))}
|
|
754
|
+
${dialRow('What counts as passing', passRuleSentence(b))}
|
|
755
|
+
${dialRow('How long a sitting runs', windowSentence(b))}
|
|
756
|
+
</ul>
|
|
757
|
+
<p class="ov-foot">As configuration: <code>${escapeHtml(JSON.stringify(b))}</code>. Changing it is a board act — propose an amendment, and it is decided under the rule above.</p>
|
|
758
|
+
`;
|
|
759
|
+
// The history and the library are panels of their own, after the live view.
|
|
680
760
|
box.innerHTML = `
|
|
681
|
-
<section class="gov-
|
|
682
|
-
<
|
|
683
|
-
<p class="
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
761
|
+
<section class="scroll" aria-labelledby="gov-history-h">
|
|
762
|
+
<h2 class="scroll__h" id="gov-history-h">How it has changed</h2>
|
|
763
|
+
<p class="scroll__lede">Every closed amendment sitting, ratified and returned alike, with the rule it was decided under.</p>
|
|
764
|
+
${history.length
|
|
765
|
+
? `<ul class="ov-rows">${history.map(historyRow).join('')}</ul>`
|
|
766
|
+
: emptyStateHtml('Never amended. This project still runs its founding constitution.')}
|
|
687
767
|
</section>
|
|
688
|
-
<h2 class="gov-h">How it has changed</h2>
|
|
689
|
-
${history.length
|
|
690
|
-
? `<div class="gov-section">${history.map(historyRow).join('')}</div>`
|
|
691
|
-
: `<p class="gov-empty">Never amended — this project still runs its founding constitution.</p>`}
|
|
692
768
|
${charterLibraryHtml(data)}
|
|
693
769
|
`;
|
|
694
770
|
}
|
|
@@ -713,9 +789,9 @@
|
|
|
713
789
|
function subjectLine(item) {
|
|
714
790
|
if (item.subject_type === 'full_idea') {
|
|
715
791
|
const title = item.subject ? item.subject.title : `idea ${item.subject_id}`;
|
|
716
|
-
return `<a href="/#/idea/${escapeHtml(String(item.subject_id))}">${escapeHtml(title)}</a>
|
|
792
|
+
return `<a href="/#/idea/${escapeHtml(String(item.subject_id))}">${escapeHtml(title)}</a> ${pill('quiet', 'Full Idea')}`;
|
|
717
793
|
}
|
|
718
|
-
return `Constitutional amendment
|
|
794
|
+
return `Constitutional amendment ${pill('quiet', 'amendment')}`;
|
|
719
795
|
}
|
|
720
796
|
|
|
721
797
|
function clockLine(item) {
|
|
@@ -732,25 +808,28 @@
|
|
|
732
808
|
return 'no clock';
|
|
733
809
|
}
|
|
734
810
|
|
|
811
|
+
// The open ballot: one voter, one dense row — who, then the direction pill
|
|
812
|
+
// and the time as facts, and an objection's section and reason as the sub line.
|
|
735
813
|
function ballotHtml(item, viewerId) {
|
|
736
814
|
if (!item.votes.length) {
|
|
737
|
-
return
|
|
815
|
+
return emptyStateHtml(`No votes yet${item.closed_at ? '. Closed unopposed' : ''}.`);
|
|
738
816
|
}
|
|
739
817
|
const rows = item.votes.map((v) => {
|
|
740
818
|
const who = escapeHtml(v.display_name || v.github_login || `builder ${v.voter_id}`);
|
|
741
819
|
const mine = String(v.voter_id) === String(viewerId) ? ' <span class="gov-held-note">your vote</span>' : '';
|
|
742
|
-
const when = escapeHtml(String(v.cast_at).slice(0, 16).replace('T', ' '));
|
|
743
820
|
// Task 1003270: the faulted section by its LABEL — the words the author
|
|
744
821
|
// wrote under and the objector picked from. `why_matters` is a jsonb key,
|
|
745
822
|
// not something a reader should have to decode.
|
|
746
823
|
const objection = v.direction === 'no'
|
|
747
|
-
? `<
|
|
824
|
+
? `<p class="ov-row__sub">objected to <strong>${escapeHtml(SECTION_LABELS[v.section_key] || v.section_key || 'the idea')}</strong>: ${escapeHtml(v.reason_md || '')}</p>`
|
|
748
825
|
: '';
|
|
749
|
-
return `<
|
|
750
|
-
<span class="
|
|
751
|
-
|
|
826
|
+
return `<li class="ov-row ov-row--dense"><div class="ov-row__main">
|
|
827
|
+
<span class="ov-row__title">${who}${mine}</span>
|
|
828
|
+
${objection}
|
|
829
|
+
<div class="ov-row__facts">${v.direction === 'no' ? pill('danger', 'objected') : pill('ok', 'yes')}<span class="ov-row__when">${when(v.cast_at)}</span></div>
|
|
830
|
+
</div></li>`;
|
|
752
831
|
}).join('');
|
|
753
|
-
return `<
|
|
832
|
+
return `<ul class="ov-rows">${rows}</ul>`;
|
|
754
833
|
}
|
|
755
834
|
|
|
756
835
|
// ---- the vote form (R17) -----------------------------------------------------
|
|
@@ -784,14 +863,12 @@
|
|
|
784
863
|
// a graded Full Idea has all five, and a blank one is only noise.
|
|
785
864
|
const body = String(fv[key] ?? '').trim();
|
|
786
865
|
if (!body) return '';
|
|
787
|
-
return `<
|
|
788
|
-
<
|
|
789
|
-
<p class="gov-idea__body">${escapeHtml(body)}</p>
|
|
790
|
-
</div>`;
|
|
866
|
+
return `<h4 class="ov-prose__l">${escapeHtml(SECTION_LABELS[key])}</h4>
|
|
867
|
+
<p class="ov-prose__p gov-idea__body">${escapeHtml(body)}</p>`;
|
|
791
868
|
}).join('');
|
|
792
869
|
return rows
|
|
793
|
-
? `<div class="gov-idea">${rows}</div>`
|
|
794
|
-
: '
|
|
870
|
+
? `<div class="ov-prose gov-idea">${rows}</div>`
|
|
871
|
+
: emptyStateHtml('This idea has no written sections.');
|
|
795
872
|
}
|
|
796
873
|
|
|
797
874
|
// ---- the sitting's rules, in plain words (task 1003270) --------------------
|
|
@@ -834,7 +911,7 @@
|
|
|
834
911
|
|
|
835
912
|
const PASS_RULE_PLAIN = {
|
|
836
913
|
first_ratifier: 'The first vote decides it: a yes ratifies, an objection returns it to its author.',
|
|
837
|
-
consent: 'It passes unless someone objects
|
|
914
|
+
consent: 'It passes unless someone objects — silence is agreement, and one reasoned objection returns it.',
|
|
838
915
|
majority: 'It passes only when more than half the board votes yes. Silence is not agreement here.',
|
|
839
916
|
};
|
|
840
917
|
|
|
@@ -844,10 +921,10 @@
|
|
|
844
921
|
const keys = membershipRankKeys(c.membership);
|
|
845
922
|
const who = keys
|
|
846
923
|
? `Every active <strong>${rankNames(keys)}</strong> ${past ? 'could vote on this' : 'may vote on this'}.`
|
|
847
|
-
: `Nobody ${past ? 'could vote' : 'may vote'}
|
|
924
|
+
: `Nobody ${past ? 'could vote' : 'may vote'} — the membership setting <code>${escapeHtml(String(c.membership || 'unset'))}</code> cannot be read.`;
|
|
848
925
|
const rule = PASS_RULE_PLAIN[c.pass_rule] || 'How this sitting passes could not be read.';
|
|
849
926
|
const clock = c.window_minutes === null || c.window_minutes === undefined
|
|
850
|
-
? `No deadline
|
|
927
|
+
? `No deadline — it ${past ? 'stayed' : 'stays'} open until a vote decides it.`
|
|
851
928
|
: `The sitting ${past ? 'ran' : 'runs'} ${fmtWindow(c.window_minutes)}${c.close_early_on_full_turnout ? ', closing early once every member has voted' : ''}.`;
|
|
852
929
|
const score = item.subject ? item.subject.completeness_score : null;
|
|
853
930
|
const grade = score == null
|
|
@@ -878,19 +955,19 @@
|
|
|
878
955
|
const isOwnIdea = item.subject_type === 'full_idea'
|
|
879
956
|
&& item.subject && String(item.subject.author_id) === String(viewerId);
|
|
880
957
|
const sections = Object.keys(SECTION_LABELS).map((key) => `
|
|
881
|
-
<label class="gov-
|
|
958
|
+
<label class="gov-vote__section"><input type="radio" name="vote-section-${escapeHtml(item.id)}" value="${escapeHtml(key)}"> <span>${escapeHtml(SECTION_LABELS[key])}</span></label>`).join('');
|
|
882
959
|
const ownNote = isOwnIdea
|
|
883
|
-
? `<p class="gov-grid-note">Your own idea. You may vote, but it can
|
|
960
|
+
? `<p class="gov-grid-note">Your own idea. You may vote, but it can’t be decided by your vote alone, and a vote on your own idea earns no karma.</p>`
|
|
884
961
|
: '';
|
|
885
962
|
return `<div class="gov-vote" data-item="${escapeHtml(item.id)}">
|
|
886
963
|
${ownNote}
|
|
887
|
-
<button type="button" class="
|
|
964
|
+
<button type="button" class="btn-accent" data-vote="yes">Vote yes</button>
|
|
888
965
|
<details class="gov-vote__no">
|
|
889
|
-
<summary class="
|
|
966
|
+
<summary class="btn-ghost">Object…</summary>
|
|
890
967
|
<p class="gov-grid-note">This sends the idea back to its author. Say why, and which section.</p>
|
|
891
|
-
<textarea
|
|
968
|
+
<textarea data-vote-reason placeholder="Why — the reason the author will act on" maxlength="5000" rows="3"></textarea>
|
|
892
969
|
<div class="gov-vote__sections">${sections}</div>
|
|
893
|
-
<button type="button" class="
|
|
970
|
+
<button type="button" class="btn-ghost btn-ghost--danger" data-vote="no">Cast objection</button>
|
|
894
971
|
</details>
|
|
895
972
|
</div>`;
|
|
896
973
|
}
|
|
@@ -906,7 +983,7 @@
|
|
|
906
983
|
try {
|
|
907
984
|
const out = await sendJson('POST', `/government/board/items/${encodeURIComponent(itemId)}/votes`, body);
|
|
908
985
|
toast(out.item_closed
|
|
909
|
-
? `Vote cast
|
|
986
|
+
? `Vote cast — the sitting closed: ${out.outcome}.`
|
|
910
987
|
: 'Vote cast.');
|
|
911
988
|
} catch (err) {
|
|
912
989
|
if (err && err.kind === '401') return;
|
|
@@ -934,15 +1011,18 @@
|
|
|
934
1011
|
//
|
|
935
1012
|
// A CLOSED sitting has no action and no idea body: its head carries the
|
|
936
1013
|
// outcome and the record lives under the same summary.
|
|
1014
|
+
//
|
|
1015
|
+
// In v3 a sitting IS the panel (task 1003469): the card is the shell's .scroll,
|
|
1016
|
+
// its title the panel's display-voice heading.
|
|
937
1017
|
function itemCard(item, viewerId) {
|
|
938
1018
|
const yes = item.votes.filter((v) => v.direction === 'yes').length;
|
|
939
1019
|
const no = item.votes.filter((v) => v.direction === 'no').length;
|
|
940
1020
|
const isOpen = !item.closed_at;
|
|
941
1021
|
const state = isOpen
|
|
942
|
-
?
|
|
943
|
-
:
|
|
1022
|
+
? `${pill('info', 'open')} <span>${escapeHtml(clockLine(item))}</span>`
|
|
1023
|
+
: (item.outcome === 'passed' ? pill('ok', item.outcome) : pill('warn', item.outcome || ''));
|
|
944
1024
|
const amendmentDetail = item.subject_type === 'constitutional_amendment' && item.subject
|
|
945
|
-
? `<p class="gov-item__amend">Proposes <code>${escapeHtml(JSON.stringify(item.subject.proposed))}</code>${item.subject.rationale_md ? `
|
|
1025
|
+
? `<p class="gov-item__amend">Proposes <code>${escapeHtml(JSON.stringify(item.subject.proposed))}</code>${item.subject.rationale_md ? ` — ${escapeHtml(item.subject.rationale_md)}` : ''}</p>`
|
|
946
1026
|
: '';
|
|
947
1027
|
const mine = item.votes.find((v) => String(v.voter_id) === String(viewerId));
|
|
948
1028
|
// A voter who has cast sees WHAT they cast where the buttons were. The line
|
|
@@ -953,8 +1033,8 @@
|
|
|
953
1033
|
: mine
|
|
954
1034
|
? `<p class="gov-item__cast">You ${mine.direction === 'no' ? 'objected' : 'voted yes'}. One vote per member.</p>`
|
|
955
1035
|
: voteFormHtml(item, viewerId);
|
|
956
|
-
const tally = `${yes} yes
|
|
957
|
-
return `<section class="gov-item" id="board-item-${escapeHtml(item.id)}">
|
|
1036
|
+
const tally = `${yes} yes · ${no} objection${no === 1 ? '' : 's'}`;
|
|
1037
|
+
return `<section class="scroll gov-item" id="board-item-${escapeHtml(item.id)}">
|
|
958
1038
|
<div class="gov-item__head">
|
|
959
1039
|
<h3 class="gov-item__title">${subjectLine(item)}</h3>
|
|
960
1040
|
<span class="gov-item__state">${state}</span>
|
|
@@ -963,7 +1043,7 @@
|
|
|
963
1043
|
${action ? `<div class="gov-item__act">${action}</div>` : ''}
|
|
964
1044
|
${isOpen ? ideaSectionsHtml(item) : ''}
|
|
965
1045
|
<details class="gov-item__meta">
|
|
966
|
-
<summary class="gov-item__summary">${tally}
|
|
1046
|
+
<summary class="gov-item__summary">${tally} · who votes, and how it ${isOpen ? 'passes' : 'passed'}</summary>
|
|
967
1047
|
<div class="gov-item__metabody">
|
|
968
1048
|
${decidedByHtml(item)}
|
|
969
1049
|
${ballotHtml(item, viewerId)}
|
|
@@ -979,25 +1059,29 @@
|
|
|
979
1059
|
data = await fetchJson('/government/board/items');
|
|
980
1060
|
} catch (err) {
|
|
981
1061
|
if (err && err.kind === '401') return;
|
|
982
|
-
box.innerHTML =
|
|
1062
|
+
box.innerHTML = emptyStateHtml('Could not load the Board Room just now.');
|
|
983
1063
|
return;
|
|
984
1064
|
}
|
|
985
1065
|
const viewerId = data.viewer_id;
|
|
986
|
-
const
|
|
987
|
-
const
|
|
1066
|
+
const openItems = data.open || [];
|
|
1067
|
+
const closedItems = data.closed || [];
|
|
1068
|
+
const open = openItems.map((i) => itemCard(i, viewerId)).join('');
|
|
1069
|
+
const closed = closedItems.map((i) => itemCard(i, viewerId)).join('');
|
|
988
1070
|
box.innerHTML =
|
|
989
|
-
(open ||
|
|
990
|
-
+ `<
|
|
991
|
-
+ (closed || `<
|
|
1071
|
+
(open || `<section class="scroll">${emptyStateHtml('Nothing is before the board. When a Full Idea clears the completeness grade, its window opens here.')}</section>`)
|
|
1072
|
+
+ `<div class="gov-board__divider">${groupHead('Closed sittings', closedItems.length)}</div>`
|
|
1073
|
+
+ (closed || `<section class="scroll">${emptyStateHtml('No sitting has closed yet.')}</section>`);
|
|
992
1074
|
// R21: the Discord announcement deep-links ?item=<id>#board-room — land the
|
|
993
1075
|
// reader on THAT sitting. Query, not hash (the hash is the tab, makeTabs
|
|
994
|
-
// owns it); a missing or unknown id is simply the room as usual.
|
|
1076
|
+
// owns it); a missing or unknown id is simply the room as usual. The mark is
|
|
1077
|
+
// the world's lit grammar: the accent, and only on the one live thing.
|
|
995
1078
|
const wanted = new URLSearchParams(window.location.search).get('item');
|
|
996
1079
|
if (wanted) {
|
|
997
1080
|
const el = document.getElementById(`board-item-${wanted}`);
|
|
998
1081
|
if (el) {
|
|
999
1082
|
el.scrollIntoView({ block: 'start' });
|
|
1000
|
-
el.style.outline = '2px solid var(--
|
|
1083
|
+
el.style.outline = '2px solid var(--accent)';
|
|
1084
|
+
el.style.outlineOffset = '2px';
|
|
1001
1085
|
}
|
|
1002
1086
|
}
|
|
1003
1087
|
// R17: the cast buttons. One delegated listener; the SERVER refuses every
|
|
@@ -1018,6 +1102,9 @@
|
|
|
1018
1102
|
document.getElementById('gov-sealed').hidden = false;
|
|
1019
1103
|
document.getElementById('gov-body').hidden = true;
|
|
1020
1104
|
document.getElementById('gov-sub').textContent = 'Restricted.';
|
|
1105
|
+
// The floor in the pack's own words (ADR 0157: metic), never a rank spelled into the source.
|
|
1106
|
+
const body = document.getElementById('gov-sealed-body');
|
|
1107
|
+
if (body) body.textContent = `Available from the ${window.OTB.rankLabel('metic')} rank up.`;
|
|
1021
1108
|
}
|
|
1022
1109
|
|
|
1023
1110
|
// Since R16 the page shell admits page.view.government (metic+), and only the
|
|
@@ -1029,8 +1116,12 @@
|
|
|
1029
1116
|
function sealRanksPanel() {
|
|
1030
1117
|
const panel = document.getElementById('gov-panel-ranks');
|
|
1031
1118
|
const body = document.getElementById('gov-body');
|
|
1032
|
-
// Replace the admin surface with the sealed
|
|
1033
|
-
panel.innerHTML =
|
|
1119
|
+
// Replace the admin surface with the sealed panel, inside its own tab only.
|
|
1120
|
+
panel.innerHTML =
|
|
1121
|
+
`<section class="scroll" aria-labelledby="gov-ranks-sealed-h">
|
|
1122
|
+
<h2 class="scroll__h" id="gov-ranks-sealed-h">${escapeHtml(window.OTB.rankLabel('archon'))}s only</h2>
|
|
1123
|
+
<p class="scroll__lede">Permissions is the Archon administration surface. The Constitution and the Board Room are yours to read.</p>
|
|
1124
|
+
</section>`;
|
|
1034
1125
|
body.hidden = false;
|
|
1035
1126
|
}
|
|
1036
1127
|
|
|
@@ -1061,7 +1152,7 @@
|
|
|
1061
1152
|
} catch (err) {
|
|
1062
1153
|
if (err && err.kind === '401') return;
|
|
1063
1154
|
if (err && err.kind === '403') { sealRanksPanel(); return; }
|
|
1064
|
-
document.getElementById('gov-grid').innerHTML =
|
|
1155
|
+
document.getElementById('gov-grid').innerHTML = emptyStateHtml('Could not load government data just now.');
|
|
1065
1156
|
}
|
|
1066
1157
|
}
|
|
1067
1158
|
|