@bongos/core 1.24.42 → 1.24.43
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 +93 -83
- package/docs/copy-inventory.md +86 -104
- package/docs/copy-registry.json +159 -351
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/primer.md +1 -1
- package/docs/page-inventory.json +3 -3
- package/docs/page-readings.json +386 -428
- package/modules/hall-ui/public/ai-studio.html +4 -4
- package/modules/hall-ui/public/atlas.html +4 -4
- package/modules/hall-ui/public/blockers.html +4 -4
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/builders.js +22 -505
- package/modules/hall-ui/public/city.html +4 -4
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/dom-utils.js +2 -1
- package/modules/hall-ui/public/goal-map.html +4 -4
- package/modules/hall-ui/public/goals.html +4 -4
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/hall-render.js +23 -524
- package/modules/hall-ui/public/hall-widgets.js +11 -109
- package/modules/hall-ui/public/idea.html +4 -4
- package/modules/hall-ui/public/ideas.html +4 -4
- package/modules/hall-ui/public/index.html +6 -19
- package/modules/hall-ui/public/index.states.json +0 -33
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch.html +4 -4
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/palette.js +7 -6
- package/modules/hall-ui/public/primer.html +4 -4
- package/modules/hall-ui/public/profile-own.js +639 -0
- package/modules/hall-ui/public/profile.css +2 -1
- package/modules/hall-ui/public/profile.html +48 -5
- package/modules/hall-ui/public/profile.js +27 -2
- package/modules/hall-ui/public/profile.states.json +18 -0
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/roadmap.html +4 -4
- package/modules/hall-ui/public/roster.html +4 -4
- package/modules/hall-ui/public/settings.html +4 -4
- package/modules/hall-ui/public/shell-trail.js +2 -1
- package/modules/hall-ui/public/shell.js +28 -25
- package/modules/hall-ui/public/street.html +4 -4
- package/modules/hall-ui/public/studio.html +4 -4
- package/modules/hall-ui/public/task.html +4 -4
- package/modules/hall-ui/public/thinking.html +4 -4
- package/modules/hall-ui/public/tweak-editor.html +4 -4
- package/modules/hall-ui/public/watch.html +4 -4
- package/modules/hall-ui/public/work.html +4 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/curve_controls_delegation.mjs +9 -8
- package/tests/curve_kind_overlay.mjs +5 -5
- package/tests/hall_acclaim_vote_control.mjs +5 -3
- package/tests/hall_audit.mjs +10 -6
- package/tests/hall_board_world.mjs +40 -26
- package/tests/hall_builders_page_pass.mjs +14 -5
- package/tests/hall_home_ships_bounded.mjs +10 -21
- package/tests/hall_kit_charts.mjs +3 -2
- package/tests/hall_loading.mjs +2 -1
- package/tests/hall_nav.mjs +36 -37
- package/tests/hall_numbers_page.mjs +100 -0
- package/tests/hall_palette.mjs +19 -24
- package/tests/hall_profile_world.mjs +7 -2
- package/tests/hall_reading_room.mjs +3 -2
- package/tests/helpers.mjs +8 -2
- package/tests/leaderboard_roles.mjs +1 -1
- package/tests/module_contributions.mjs +6 -4
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/page_tweak_artist.mjs +2 -1
- package/tests/rank_tier_single_source.mjs +3 -0
- package/tests/task_visuals.mjs +3 -1
|
@@ -25,12 +25,11 @@
|
|
|
25
25
|
const { moduleOn } = window.OTB;
|
|
26
26
|
|
|
27
27
|
// ---- tiny helpers --------------------------------------------------------
|
|
28
|
-
// escapeHtml / $ / fmtNum /
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
28
|
+
// escapeHtml / $ / fmtNum / rankLabel / toast now come from the shared
|
|
29
|
+
// window.OTB toolkit (dom-utils.js, loaded first). Thin wrappers keep this
|
|
30
|
+
// file's exact variants ('—' rankLabel empty; a 2200ms clearing toast).
|
|
31
|
+
// renderRankBadge + RANK_ORDER stay local below.
|
|
32
32
|
const { escapeHtml, $, fmtNum } = window.OTB;
|
|
33
|
-
const fmtDate = (iso) => window.OTB.fmtDate(iso);
|
|
34
33
|
const rankLabel = (rank) => window.OTB.rankLabel(rank);
|
|
35
34
|
const toast = (msg) => window.OTB.toast(msg, { duration: 2200, clear: true });
|
|
36
35
|
|
|
@@ -64,16 +63,6 @@
|
|
|
64
63
|
return res.data;
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
async function patchJSON(path, body) {
|
|
68
|
-
const res = await api.request('PATCH', path, { body });
|
|
69
|
-
if (res.status === 401) {
|
|
70
|
-
window.location.assign(`${API}/auth/web/start`);
|
|
71
|
-
throw new Error('redirecting to auth');
|
|
72
|
-
}
|
|
73
|
-
if (!res.ok) throw new Error(`${path} → ${res.status}`);
|
|
74
|
-
return res.data;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
66
|
async function postJSON(path, body) {
|
|
78
67
|
const res = await api.request('POST', path, { body });
|
|
79
68
|
if (res.status === 401) {
|
|
@@ -104,27 +93,8 @@
|
|
|
104
93
|
return res.data;
|
|
105
94
|
}
|
|
106
95
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
// emptyRow — the placeholder/error <tr> every ledger table renders when it has
|
|
110
|
-
// nothing (or failed) to show. `message` is plain copy (escaped defensively
|
|
111
|
-
// even though current callers pass static literals).
|
|
112
|
-
function emptyRow(colspan, message) {
|
|
113
|
-
return `<tr><td colspan="${colspan}" class="ledger__empty">${escapeHtml(message)}</td></tr>`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// signClasses — the up/down/flat BEM modifier for a signed number. `prefix` is
|
|
117
|
-
// the block, e.g. signClasses(delta, 'karma-feed__delta') → 'karma-feed__delta--up'.
|
|
118
|
-
// The +/- TEXT differs per feed (fmtNum vs raw int vs '±0'), so callers format
|
|
119
|
-
// the displayed value themselves.
|
|
120
|
-
function signClasses(n, prefix) {
|
|
121
|
-
const v = Number(n) || 0;
|
|
122
|
-
return `${prefix}--${v > 0 ? 'up' : v < 0 ? 'down' : 'flat'}`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// (task 2174: the hand-rolled renderPager the ships + roster tables shared is
|
|
126
|
-
// gone — both now use the OTBKit pager, which clamps, persists per-page and
|
|
127
|
-
// paints the bar from one implementation.)
|
|
96
|
+
// (task 1005050: emptyRow and signClasses went to profile-own.js with the
|
|
97
|
+
// ledgers and the karma feed that used them; nothing on Home draws one.)
|
|
128
98
|
|
|
129
99
|
// ---- repo info (the clone URL the setup screen hands a newcomer) ---
|
|
130
100
|
//
|
|
@@ -222,26 +192,12 @@
|
|
|
222
192
|
// per-widget gates (archonOnly) carried on the registry entry.
|
|
223
193
|
|
|
224
194
|
// task 1723: which container a widget mounts into. A widget may declare
|
|
225
|
-
// `zone` (a fine-grained Home slot)
|
|
226
|
-
//
|
|
227
|
-
// zone so pre-redesign module widgets still land somewhere visible. Every
|
|
195
|
+
// `zone` (a fine-grained Home slot) or `view` (only 'home' since task 1005050
|
|
196
|
+
// folded the Standing view into the Profile page); both default to the Home
|
|
197
|
+
// main zone so pre-redesign module widgets still land somewhere visible. Every
|
|
228
198
|
// container lives INSIDE <main>, so the `main .scroll` sweeps (newcomer gate,
|
|
229
199
|
// landing hide-all) keep working.
|
|
230
200
|
|
|
231
|
-
// task 2174: Standing is an identity band + four rail sections grouped by the
|
|
232
|
-
// question they answer. This array is the rail's order and its labels; each
|
|
233
|
-
// entry gets a #standing-<key> anchor container inside #zone-standing that the
|
|
234
|
-
// group's widgets mount into. The Profile card declares no group, so it stays
|
|
235
|
-
// above them as the band.
|
|
236
|
-
|
|
237
|
-
// Create ALL four group containers the first time any group is asked for, so
|
|
238
|
-
// the rail's order is STANDING_GROUPS' — not whichever group's first widget
|
|
239
|
-
// happens to register earliest. Creating them on demand (rather than up front)
|
|
240
|
-
// is what keeps the Profile band on top: an ungrouped Standing widget appends
|
|
241
|
-
// straight to #zone-standing, so anything registered before the first grouped
|
|
242
|
-
// widget lands above the groups. Idempotent.
|
|
243
|
-
|
|
244
|
-
|
|
245
201
|
// mountHallWidgets — stamp every registered widget's <section> into its view
|
|
246
202
|
// container (falling back to <main>), in registry order, then render its
|
|
247
203
|
// skeleton via mount(host). Idempotent: a second call (defensive) re-uses an
|
|
@@ -257,14 +213,9 @@
|
|
|
257
213
|
// The budgets themselves become a CI fitness assertion in task 2177.
|
|
258
214
|
|
|
259
215
|
|
|
260
|
-
//
|
|
261
|
-
// the
|
|
262
|
-
//
|
|
263
|
-
// so the same instance survives re-renders.
|
|
264
|
-
// The rail (#standing-rail) has one entry per GROUP that currently has
|
|
265
|
-
// something to show, not one per widget. A group with every member hidden
|
|
266
|
-
// (a newcomer, an empty karma feed) hides itself and drops off the rail.
|
|
267
|
-
// Keeps its (builder, onboarding) signature — all call sites pass them.
|
|
216
|
+
// (The Standing view's section rail lived in renderNav until task 1005050
|
|
217
|
+
// folded the view into the Profile page.) renderNav keeps its (builder,
|
|
218
|
+
// onboarding) signature — all call sites pass them.
|
|
268
219
|
|
|
269
220
|
// #748: newcomer gate. A Xenos sees a deliberately spare hall — their
|
|
270
221
|
// onboarding path and their Standing card — until they graduate to Thetes.
|
|
@@ -315,358 +266,9 @@
|
|
|
315
266
|
// renderDiscordLink / unlinkDiscord (F7, ADR 0033) moved to
|
|
316
267
|
// public-builders/settings.js — these controls now live on /builders/settings.
|
|
317
268
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.map((el) => el.value);
|
|
322
|
-
const btn = $('#disc-save');
|
|
323
|
-
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
|
|
324
|
-
try {
|
|
325
|
-
const resp = await patchJSON(`${API}/me/disciplines`, { preferred_disciplines: selected });
|
|
326
|
-
renderProfile(resp.builder);
|
|
327
|
-
toast(selected.length ? `Preferred work: ${selected.join(', ')}` : 'Preference cleared.');
|
|
328
|
-
} catch (err) {
|
|
329
|
-
if (err && err.message === 'redirecting to auth') return;
|
|
330
|
-
console.error('[builders] save disciplines failed', err);
|
|
331
|
-
toast('Could not save your preference. Try again.');
|
|
332
|
-
if (btn) { btn.disabled = false; btn.textContent = 'Save'; }
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// ---- achievements --------------------------------------------------------
|
|
337
|
-
|
|
338
|
-
function renderAchievements(all, unlocked, lockedWithProgress) {
|
|
339
|
-
const unlockedById = new Map(unlocked.map((u) => [u.achievement_id, u]));
|
|
340
|
-
// Map achievement_id → progress_text for locked entries. Falsy/empty
|
|
341
|
-
// progress just shows the credit-bonus line as before.
|
|
342
|
-
const progressById = new Map(
|
|
343
|
-
(lockedWithProgress || []).map((l) => [l.achievement_id, l.progress_text || '']),
|
|
344
|
-
);
|
|
345
|
-
const grid = $('#ach-grid');
|
|
346
|
-
if (!all.length) {
|
|
347
|
-
grid.innerHTML = `<div class="profile__placeholder">No achievements earned yet.</div>`;
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
grid.innerHTML = all.map((a) => {
|
|
351
|
-
const got = unlockedById.get(a.id);
|
|
352
|
-
const cls = got ? 'ach' : 'ach ach--locked';
|
|
353
|
-
const bonus = a.credit_bonus ? `<p class="ach__bonus">+${fmtNum(a.credit_bonus)} ${(window.OTBBranding?.currencyLabel?.() ?? 'credits')}</p>` : '';
|
|
354
|
-
const when = got ? `<p class="ach__bonus">Won ${fmtDate(got.unlocked_at)}</p>` : '';
|
|
355
|
-
const progress = !got && progressById.has(a.id) && progressById.get(a.id)
|
|
356
|
-
? `<p class="ach__progress">${escapeHtml(progressById.get(a.id))}</p>`
|
|
357
|
-
: '';
|
|
358
|
-
return `
|
|
359
|
-
<div class="${cls}" title="${escapeHtml(a.id)}">
|
|
360
|
-
<div class="ach__icon" aria-hidden="true">${escapeHtml(a.icon || '✦')}</div>
|
|
361
|
-
<div class="ach__body">
|
|
362
|
-
<p class="ach__name">${escapeHtml(a.name)}</p>
|
|
363
|
-
<p class="ach__desc">${escapeHtml(a.description || '')}</p>
|
|
364
|
-
${when || bonus}
|
|
365
|
-
${progress}
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
368
|
-
`;
|
|
369
|
-
}).join('');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// ---- recent ships --------------------------------------------------------
|
|
373
|
-
|
|
374
|
-
// All shipped works for this builder, fetched once via ?limit=all then
|
|
375
|
-
// filtered, sorted, and paged entirely client-side. Mirrors the status site's
|
|
376
|
-
// Lately Inscribed pattern (modules/status-ui/public/status.js renderShippedPage), with
|
|
377
|
-
// an additional version filter since a builder's history spans versions.
|
|
378
|
-
// task 2174: paging moved to the OTBKit pager (per-page select + persistence);
|
|
379
|
-
// 10 stays the default page size this table has always used.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// task 1957 — a shipped row's payout is the ACTUAL ledger figure (credits_awarded),
|
|
383
|
-
// not the creation-time estimate. taskCreditValue picks the right one by status.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
// Re-render when filters change. Reset to page 1 so the new filter starts at
|
|
388
|
-
// the top — otherwise switching to a smaller window can leave you on page 9
|
|
389
|
-
// of a 2-page result.
|
|
390
|
-
function onShipsFilterChange() {
|
|
391
|
-
shipsPager.page = 1;
|
|
392
|
-
renderRecentShips();
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// ---- recent grades (task 197) -------------------------------------------
|
|
396
|
-
|
|
397
|
-
function renderRecentGrades(grades) {
|
|
398
|
-
const tbody = $('#recent-grades tbody');
|
|
399
|
-
if (!tbody) return;
|
|
400
|
-
if (!grades || !grades.length) {
|
|
401
|
-
tbody.innerHTML = emptyRow(5, 'No work reviewed yet.');
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
tbody.innerHTML = grades.map((g) => {
|
|
405
|
-
// A grader OUTAGE row (panel_outcome 'unavailable') carries a placeholder
|
|
406
|
-
// score of 0 — label it, never render it as a 0.0 fail (task 1002652).
|
|
407
|
-
const isOutage = g.panel_outcome === 'unavailable';
|
|
408
|
-
// A json-null gating axis (task 1003318 — the Quality worker never ran)
|
|
409
|
-
// is likewise not a judgment: the score is the surviving axis, not a fail.
|
|
410
|
-
const isLensOut = !isOutage && !!g.rubric_scores && g.rubric_scores.code_quality === null;
|
|
411
|
-
const score = isOutage || isLensOut ? '—' : Number(g.score).toFixed(1);
|
|
412
|
-
// A verdict is verification state: it wears the fixed vocabulary through
|
|
413
|
-
// .ledger__verdict--* (style.css), never an inline colour. The literals
|
|
414
|
-
// that stood here painted `passed` from a --olive that is not a token of
|
|
415
|
-
// the world, so every hall ever rendered it as the fallback (task 1003495).
|
|
416
|
-
const verdict = isOutage
|
|
417
|
-
? `<span class="ledger__verdict--none">not graded (grader outage)</span>`
|
|
418
|
-
: isLensOut
|
|
419
|
-
? `<span class="ledger__verdict--none">not graded (quality lens unavailable)</span>`
|
|
420
|
-
: g.passed
|
|
421
|
-
? `<span class="ledger__verdict--ok">passed</span>`
|
|
422
|
-
: `<span class="ledger__verdict--danger">fell short</span>`;
|
|
423
|
-
return `
|
|
424
|
-
<tr>
|
|
425
|
-
<td class="ledger__num">#${g.task_id}</td>
|
|
426
|
-
<td>${escapeHtml(g.title || '')}</td>
|
|
427
|
-
<td class="ledger__drachmae">${isOutage || isLensOut ? '—' : `${score} / 10`}</td>
|
|
428
|
-
<td>${verdict}</td>
|
|
429
|
-
<td class="ledger__date">${fmtDate(g.graded_at)}</td>
|
|
430
|
-
</tr>
|
|
431
|
-
`;
|
|
432
|
-
}).join('');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function renderRollingNote(rolling) {
|
|
436
|
-
const note = $('#grades-rolling-note');
|
|
437
|
-
if (!note) return;
|
|
438
|
-
if (!rolling || !rolling.n || Number(rolling.n) === 0) {
|
|
439
|
-
note.hidden = true;
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
const avg = Number(rolling.avg_score).toFixed(1);
|
|
443
|
-
const passed = Number(rolling.n_passed || 0);
|
|
444
|
-
const n = Number(rolling.n);
|
|
445
|
-
note.textContent = `Last 14 days: ${avg} / 10 average across ${n} graded work${n === 1 ? '' : 's'} (${passed} passed).`;
|
|
446
|
-
note.hidden = false;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// ---- the dev curve (V3.R72 #291, C5) ------------------------------------
|
|
450
|
-
//
|
|
451
|
-
// Per-builder historical analytics: ship rate, cost-per-task, duration, and
|
|
452
|
-
// grade pass-rate over time, plus a top-kinds fingerprint. Charts are inline
|
|
453
|
-
// SVG sparklines — no chart library (the task spec's explicit constraint).
|
|
454
|
-
// An Archon additionally gets a "You / All builders" scope toggle backed by
|
|
455
|
-
// the ecosystem aggregate (individuals stripped), satisfying the
|
|
456
|
-
// cross-builder view requirement without a per-builder roster on this page.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// Build a tiny SVG line sparkline from an array of numeric y-values. Returns
|
|
460
|
-
// a self-contained <svg> string. A flat/short series degrades to a baseline.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const pct = (v) => (v == null ? '—' : `${Math.round(Number(v) * 100)}%`);
|
|
465
|
-
const usd = (v) => `$${(Number(v) || 0).toFixed(2)}`;
|
|
466
|
-
const mins = (v) => (v == null ? '—' : `${Math.round(Number(v))}m`);
|
|
467
|
-
|
|
468
|
-
// Render the curve for whichever scope is active. `data` is either the self
|
|
469
|
-
// payload (/analytics/builder/:id) or the ecosystem payload, normalized below.
|
|
470
|
-
|
|
471
|
-
// Fire-and-forget loader: fetch the builder's own curve (and, for an Archon,
|
|
472
|
-
// the ecosystem aggregate) and render. A failed/empty feed simply leaves the
|
|
473
|
-
// section hidden rather than holding up the rest of the hall.
|
|
474
|
-
async function loadAnalytics(builder) {
|
|
475
|
-
const scroll = $('#analytics-scroll');
|
|
476
|
-
if (!scroll || !builder?.id) return;
|
|
477
|
-
analyticsState.builderId = builder.id;
|
|
478
|
-
analyticsState.scope = 'self';
|
|
479
|
-
analyticsState.kind = null;
|
|
480
|
-
try {
|
|
481
|
-
const self = await getJSON(`${API}/analytics/builder/${builder.id}?days=90`);
|
|
482
|
-
analyticsState.self = self;
|
|
483
|
-
// The picker's options come from THIS first, unfiltered load and are then
|
|
484
|
-
// left alone — see kindFilterHtml. A later filtered payload's top_kinds is
|
|
485
|
-
// deliberately not allowed to overwrite them.
|
|
486
|
-
analyticsState.kinds = Array.isArray(self?.top_kinds) ? self.top_kinds : [];
|
|
487
|
-
// A gated newcomer with nothing shipped: keep the section hidden.
|
|
488
|
-
if (!self || (self.headline?.total_shipped || 0) === 0) { scroll.hidden = true; return; }
|
|
489
|
-
scroll.hidden = false;
|
|
490
|
-
renderCurveBody();
|
|
491
|
-
renderNav(builder, lastMeState?.onboarding || null); // re-show the Curve chip now that the section is visible
|
|
492
|
-
// THE BENCHMARK IS FOR EVERY BUILDER, NOT JUST THE ARCHON (task 1004005).
|
|
493
|
-
// This fetch used to sit behind `if (analyticsState.isArchon)`, so the
|
|
494
|
-
// ecosystem average a builder is meant to measure themselves against was
|
|
495
|
-
// fetched for exactly one of them. The route is `rank: public` — no auth
|
|
496
|
-
// middleware, CORS '*', per-builder identity stripped server-side — and the
|
|
497
|
-
// unauthenticated status page already reads it, so the gate protected
|
|
498
|
-
// nothing and only withheld the comparison. Still fire-and-forget: the
|
|
499
|
-
// ecosystem feed is optional context, never a reason to hold up the curve.
|
|
500
|
-
getJSON(`${API}/public/analytics/ecosystem?days=90`)
|
|
501
|
-
// ALWAYS re-render, not only in the 'eco' scope. That condition was
|
|
502
|
-
// correct while the two scopes merely SWAPPED which single series was
|
|
503
|
-
// drawn; now the ecosystem series is also the benchmark overlaid on the
|
|
504
|
-
// 'self' view, so a self-scoped reader who arrived before this resolved
|
|
505
|
-
// would sit with no overlay until they touched an unrelated control.
|
|
506
|
-
.then((eco) => { analyticsState.eco = eco; renderCurveBody(); })
|
|
507
|
-
.catch(() => { /* ecosystem optional */ });
|
|
508
|
-
} catch (err) {
|
|
509
|
-
console.error('[builders] analytics load failed', err);
|
|
510
|
-
scroll.hidden = true;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Re-fetch BOTH series under a kind filter (task 1004005). Both, always: an
|
|
515
|
-
// overlay that narrowed one side and not the other would compare one kind of
|
|
516
|
-
// work against all work and still look like a like-for-like benchmark — the
|
|
517
|
-
// failure this task's own text calls worse than no benchmark at all.
|
|
518
|
-
//
|
|
519
|
-
// `kinds` is deliberately not refreshed here; see kindFilterHtml.
|
|
520
|
-
let curveKindSeq = 0;
|
|
521
|
-
async function reloadCurveForKind(kind) {
|
|
522
|
-
const id = analyticsState.builderId;
|
|
523
|
-
if (!id) return;
|
|
524
|
-
analyticsState.kind = kind;
|
|
525
|
-
// A dropdown can be changed faster than a round trip returns, and the two
|
|
526
|
-
// fetches can land out of order. Only the newest request may write state.
|
|
527
|
-
const seq = ++curveKindSeq;
|
|
528
|
-
const q = kind ? `&kind=${encodeURIComponent(kind)}` : '';
|
|
529
|
-
renderCurveBody(); // reflect the new selection immediately
|
|
530
|
-
try {
|
|
531
|
-
const [self, eco] = await Promise.all([
|
|
532
|
-
getJSON(`${API}/analytics/builder/${id}?days=90${q}`),
|
|
533
|
-
getJSON(`${API}/public/analytics/ecosystem?days=90${q}`).catch(() => null),
|
|
534
|
-
]);
|
|
535
|
-
if (seq !== curveKindSeq) return;
|
|
536
|
-
analyticsState.self = self;
|
|
537
|
-
if (eco) analyticsState.eco = eco;
|
|
538
|
-
renderCurveBody();
|
|
539
|
-
} catch (err) {
|
|
540
|
-
if (seq !== curveKindSeq) return;
|
|
541
|
-
console.error('[builders] curve kind filter failed', err);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// ---- karma feed (V3.R26 #246) -------------------------------------------
|
|
546
|
-
//
|
|
547
|
-
// The recent karma_log entries for the current builder. The /me payload
|
|
548
|
-
// already includes them under `karma.recent`. The scroll stays hidden when
|
|
549
|
-
// the builder has no history yet (zero karma is a common starting state,
|
|
550
|
-
// so we don't want to push an empty table at every new citizen).
|
|
551
|
-
|
|
552
|
-
function renderKarmaFeed(entries) {
|
|
553
|
-
const scroll = $('#karma-scroll');
|
|
554
|
-
const tbody = $('#karma-feed tbody');
|
|
555
|
-
if (!scroll || !tbody) return;
|
|
556
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
557
|
-
// Hide the scroll entirely if the builder has no karma history.
|
|
558
|
-
// Keeps the page tidy for fresh citizens; reappears the moment a
|
|
559
|
-
// karma_log row lands.
|
|
560
|
-
scroll.hidden = true;
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
// #748: a gated newcomer never shows the karma feed, even if karma exists.
|
|
564
|
-
scroll.hidden = newcomerGated;
|
|
565
|
-
tbody.innerHTML = entries.map((k) => {
|
|
566
|
-
const delta = Number(k.delta) || 0;
|
|
567
|
-
const deltaCls = signClasses(delta, 'karma-feed__delta');
|
|
568
|
-
const deltaText = delta > 0 ? `+${fmtNum(delta)}` : fmtNum(delta);
|
|
569
|
-
const granted = k.granted_by
|
|
570
|
-
? `@${escapeHtml(k.granted_by_login || k.granted_by_display_name || `builder #${k.granted_by}`)}`
|
|
571
|
-
: '<span style="opacity:0.6;">— all builders —</span>';
|
|
572
|
-
return `
|
|
573
|
-
<tr>
|
|
574
|
-
<td class="ledger__date">${escapeHtml(fmtDate(k.recorded_at))}</td>
|
|
575
|
-
<td class="karma-feed__delta ${deltaCls}">${deltaText}</td>
|
|
576
|
-
<td>${escapeHtml(k.reason || '')}</td>
|
|
577
|
-
<td>${granted}</td>
|
|
578
|
-
</tr>
|
|
579
|
-
`;
|
|
580
|
-
}).join('');
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// ---- peer acclaim feed (V3.R25 #245) ------------------------------------
|
|
584
|
-
//
|
|
585
|
-
// Other builders' recently confirmed/shipped work, each with a +/- vote
|
|
586
|
-
// widget. Votes fold into the shipping builder's karma on a weekly tally,
|
|
587
|
-
// lagged 7 days (server-enforced). Metic+ may vote; the endpoint returns
|
|
588
|
-
// can_vote so Xenos see the tallies read-only. The endpoint already excludes
|
|
589
|
-
// the caller's own ships (anti-self-vote), so there's nothing to vote on that
|
|
590
|
-
// isn't a peer's work.
|
|
591
|
-
|
|
592
|
-
let acclaimCanVote = false;
|
|
593
|
-
// task 1002446: this was the hall's last unpaged list. The endpoint returns the
|
|
594
|
-
// whole feed in one call, so the rows are held here and the kit's client-side
|
|
595
|
-
// pager slices them — a page change re-renders without a refetch.
|
|
596
|
-
const acclaimState = { tasks: [] };
|
|
597
|
-
const acclaimPager = window.OTBKit.makePager('acclaim', 'otb.standing.acclaimPerPage', { defaultPerPage: 10 });
|
|
598
|
-
|
|
599
|
-
function renderAcclaimFeed(data) {
|
|
600
|
-
const scroll = $('#acclaim-scroll');
|
|
601
|
-
if (!scroll) return;
|
|
602
|
-
const tasks = (data && Array.isArray(data.tasks)) ? data.tasks : [];
|
|
603
|
-
acclaimState.tasks = tasks;
|
|
604
|
-
acclaimCanVote = !!(data && data.can_vote);
|
|
605
|
-
if (tasks.length === 0) {
|
|
606
|
-
// Nothing to acclaim yet (e.g. the only confirmed work so far is your
|
|
607
|
-
// own). Hide rather than show an empty ledger.
|
|
608
|
-
scroll.hidden = true;
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
// #748: a gated newcomer never shows the acclaim feed.
|
|
612
|
-
scroll.hidden = newcomerGated;
|
|
613
|
-
// task 2174: this feed lands after loadAll built the rail, and it can be the
|
|
614
|
-
// only visible member of the Karma group (an empty karma feed hides itself).
|
|
615
|
-
// Re-run renderNav so the group — and its rail entry — appear.
|
|
616
|
-
if (lastMeState?.builder) renderNav(lastMeState.builder, lastMeState.onboarding || null);
|
|
617
|
-
renderAcclaimRows();
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function renderAcclaimRows() {
|
|
621
|
-
const tbody = $('#acclaim-feed tbody');
|
|
622
|
-
if (!tbody) return;
|
|
623
|
-
const slice = window.OTBKit.paginate(acclaimState.tasks, acclaimPager);
|
|
624
|
-
tbody.innerHTML = slice.map((t) => {
|
|
625
|
-
// task 1004432: an approved page is its artist's; the applying session rides along as a helper.
|
|
626
|
-
const helperName = t.helper_display_name || (t.helper_login ? `@${t.helper_login}` : '');
|
|
627
|
-
const by = escapeHtml(t.display_name || (t.github_login ? `@${t.github_login}` : `builder #${t.shipped_by}`)) + (helperName ? ` <span class="acclaim__evidence">with help from ${escapeHtml(helperName)}</span>` : '');
|
|
628
|
-
// task 1004833: the votes are THE shared list control (vote-control.js),
|
|
629
|
-
// pre-painted from the feed's own net_votes/my_vote so it costs no extra
|
|
630
|
-
// read. A viewer below the vote floor sees the net alone.
|
|
631
|
-
const net = Number(t.net_votes) || 0;
|
|
632
|
-
const votes = acclaimCanVote && window.OTBVote
|
|
633
|
-
? window.OTBVote.taskHtml(t)
|
|
634
|
-
: `<span class="vote__net" title="Metic rank and above may vote">${net > 0 ? `+${net}` : `${net}`}</span>`;
|
|
635
|
-
const title = escapeHtml(t.value_summary || t.title || `task #${t.id}`);
|
|
636
|
-
return `
|
|
637
|
-
<tr>
|
|
638
|
-
<td>${title}${acclaimEvidenceHtml(t)}</td>
|
|
639
|
-
<td>${by}</td>
|
|
640
|
-
<td>${votes}</td>
|
|
641
|
-
</tr>
|
|
642
|
-
`;
|
|
643
|
-
}).join('');
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
async function loadAcclaimFeed() {
|
|
647
|
-
try {
|
|
648
|
-
const data = await getJSON(`${API}/tasks/peer-votes/feed?limit=25`);
|
|
649
|
-
renderAcclaimFeed(data);
|
|
650
|
-
} catch (err) {
|
|
651
|
-
// Non-fatal: a feed failure shouldn't take down the hall.
|
|
652
|
-
console.warn('[hall] acclaim feed failed:', err.message);
|
|
653
|
-
const scroll = $('#acclaim-scroll');
|
|
654
|
-
if (scroll) scroll.hidden = true;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// task 1004833: the shared vote control casts the vote and repaints its own
|
|
659
|
-
// row; it then raises 'otb-vote' with the server's answer. The feed is paged,
|
|
660
|
-
// so the vote must land in the HELD rows too (task 1002446), or flipping to
|
|
661
|
-
// page 2 and back re-renders the stale tally over the one just cast.
|
|
662
|
-
function onAcclaimVote(ev) {
|
|
663
|
-
const d = ev && ev.detail;
|
|
664
|
-
if (!d || d.kind !== 'task') return;
|
|
665
|
-
const taskIdNum = Number(d.id);
|
|
666
|
-
acclaimState.tasks = acclaimState.tasks.map((t) => (Number(t.id) === taskIdNum
|
|
667
|
-
? { ...t, net_votes: d.net_votes, my_vote: d.my_vote }
|
|
668
|
-
: t));
|
|
669
|
-
}
|
|
269
|
+
// (task 1005050: the achievements grid, the ship ledger, reviews, the dev curve
|
|
270
|
+
// and the karma and peer-recognition feeds were the Accomplishments view, which
|
|
271
|
+
// folded into the Profile page: profile-own.js draws the private half there.)
|
|
670
272
|
|
|
671
273
|
// ---- task browser (removed #750) -----------------------------------------
|
|
672
274
|
// The claimable-works list now lives solely on the /work Work Board (#748);
|
|
@@ -799,20 +401,6 @@
|
|
|
799
401
|
// #731: the CLI-token re-issue card (V3.R91 #401) and the Discord connect/
|
|
800
402
|
// link card (F7, ADR 0033) moved to /builders/settings — the hall's Standing
|
|
801
403
|
// card is identity-only now.
|
|
802
|
-
renderRecentGrades(me.grades?.recent || []);
|
|
803
|
-
renderRollingNote(me.grades?.rolling || null);
|
|
804
|
-
// V3.R72 (#291, C5): the dev curve. Loaded fire-and-forget — a slow or
|
|
805
|
-
// empty analytics feed must not hold up the rest of the hall, and the
|
|
806
|
-
// section un-hides itself (and re-renders the nav) only when there's data.
|
|
807
|
-
loadAnalytics(me.builder);
|
|
808
|
-
// V3.R26 (#246): karma feed — the recent karma_log entries powering the
|
|
809
|
-
// "why did my karma move" answer. The /me payload bundles this so the
|
|
810
|
-
// page doesn't need a follow-up call.
|
|
811
|
-
renderKarmaFeed(me.karma?.recent || []);
|
|
812
|
-
// V3.R25 (#245): peer acclaim feed — a separate endpoint (not bundled in
|
|
813
|
-
// /me because it spans all builders' work), loaded fire-and-forget so a
|
|
814
|
-
// slow/failed feed can't hold up the rest of the hall.
|
|
815
|
-
loadAcclaimFeed();
|
|
816
404
|
// #408: build the sticky nav now that every section has settled its
|
|
817
405
|
// hidden state (karma → security/roster).
|
|
818
406
|
// The renderNav call also wires the IntersectionObserver for active-
|
|
@@ -832,41 +420,15 @@
|
|
|
832
420
|
// down the page — log and show an inline placeholder.
|
|
833
421
|
// #726: the per-skill model picker (Voices) moved to /builders/settings,
|
|
834
422
|
// so the hall no longer fetches /me/skill-prefs here.
|
|
835
|
-
const [
|
|
836
|
-
|
|
837
|
-
//
|
|
838
|
-
// which only the Accomplishments ledger fetches (hall-render.js loadShipLedger).
|
|
423
|
+
const [ships, lb] = await Promise.allSettled([
|
|
424
|
+
// task 1004854: Home's newest few + a count — never the whole history
|
|
425
|
+
// (the Profile page's Works ledger reads that, since task 1005050).
|
|
839
426
|
getJSON(`${API}/me/recent-ships?limit=${HOME_SHIPS_LIMIT}`),
|
|
840
|
-
getJSON(`${API}/versions`),
|
|
841
427
|
getJSON(`${API}/public/leaderboard`), // task 1004437: the list with each builder's roles
|
|
842
428
|
]);
|
|
843
429
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
// for V2, so the slice is defensive.) Unlocked + locked-with-progress come
|
|
847
|
-
// straight from the enriched /me payload.
|
|
848
|
-
renderAchievements(
|
|
849
|
-
(achAll.value.achievements || []).slice(0, 10),
|
|
850
|
-
me.unlocked || [],
|
|
851
|
-
me.locked_with_progress || [],
|
|
852
|
-
);
|
|
853
|
-
} else {
|
|
854
|
-
$('#ach-grid').innerHTML = `<div class="profile__placeholder">Achievements could not be loaded.</div>`;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
if (ships.status === 'fulfilled') {
|
|
858
|
-
setRecentShips(ships.value);
|
|
859
|
-
} else {
|
|
860
|
-
$('#recent-ships tbody').innerHTML = emptyRow(4, 'Your work history could not be loaded.');
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
if (versionsResp.status === 'fulfilled') {
|
|
864
|
-
const versions = versionsResp.value.versions || [];
|
|
865
|
-
// Populate the Works Wrought version filter — limited to versions this
|
|
866
|
-
// builder has actually shipped to, so the dropdown can't offer empty
|
|
867
|
-
// options. (The old claimable-works version filter moved to /work, #750.)
|
|
868
|
-
populateShipVersionFilter(versions);
|
|
869
|
-
}
|
|
430
|
+
// Only the Standing chip's shipped count reads this; a failure leaves its dash.
|
|
431
|
+
if (ships.status === 'fulfilled') setRecentShips(ships.value);
|
|
870
432
|
|
|
871
433
|
if (lb.status === 'fulfilled') {
|
|
872
434
|
lbState.rows = lb.value.leaderboard || [];
|
|
@@ -883,51 +445,6 @@
|
|
|
883
445
|
}
|
|
884
446
|
|
|
885
447
|
function wireEvents() {
|
|
886
|
-
// #profile-body is re-rendered after load, so delegate the save click.
|
|
887
|
-
$('#profile-body').addEventListener('click', (e) => {
|
|
888
|
-
if (e.target.closest('#disc-save')) saveDisciplines();
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
// V3.R72 (#291): the dev-curve scope toggle. Delegated because
|
|
892
|
-
// renderCurveBody replaces the toggle's innerHTML — and from the whole
|
|
893
|
-
// Progress section, NOT #curve-body: the controls render into
|
|
894
|
-
// #curve-scope-note in the section's lede, a sibling of #curve-body, so a
|
|
895
|
-
// listener there never heard them (task 1004079).
|
|
896
|
-
$('#analytics-scroll')?.addEventListener('click', (e) => {
|
|
897
|
-
const btn = e.target.closest('.curve__scope-btn[data-scope]');
|
|
898
|
-
if (!btn) return;
|
|
899
|
-
const scope = btn.dataset.scope === 'eco' ? 'eco' : 'self';
|
|
900
|
-
if (scope === analyticsState.scope) return;
|
|
901
|
-
analyticsState.scope = scope;
|
|
902
|
-
renderCurveBody();
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
// The kind filter (task 1004005). Delegated like the scope toggle, and for
|
|
906
|
-
// the same reason: renderCurveBody replaces the control's innerHTML, so a
|
|
907
|
-
// listener bound to the element itself would not survive the first render.
|
|
908
|
-
// 'change', not 'input' — a keyboard user arrowing through a native select
|
|
909
|
-
// fires input per option, and each one would be a pair of round trips.
|
|
910
|
-
$('#analytics-scroll')?.addEventListener('change', (e) => {
|
|
911
|
-
const sel = e.target.closest('#curve-kind');
|
|
912
|
-
if (!sel) return;
|
|
913
|
-
const kind = sel.value ? sel.value : null;
|
|
914
|
-
if (kind === analyticsState.kind) return;
|
|
915
|
-
reloadCurveForKind(kind);
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
// task 1004833: the shared vote control reports each landed vote to the feed.
|
|
919
|
-
$('#acclaim-feed')?.addEventListener('otb-vote', onAcclaimVote);
|
|
920
|
-
|
|
921
|
-
$('#f-ships-window')?.addEventListener('change', onShipsFilterChange);
|
|
922
|
-
$('#f-ships-version')?.addEventListener('change', onShipsFilterChange);
|
|
923
|
-
$('#f-ships-sort')?.addEventListener('change', renderRecentShips);
|
|
924
|
-
// task 2174: the kit wires prev/next/per-page for both tables against the
|
|
925
|
-
// #ships-*/#lb-* ids its own pagerBarHtml() stamped in hall-widgets.js.
|
|
926
|
-
window.OTBKit.wirePager(shipsPager, renderRecentShips);
|
|
927
|
-
|
|
928
|
-
window.OTBKit.wirePager(acclaimPager, renderAcclaimRows);
|
|
929
|
-
|
|
930
|
-
|
|
931
448
|
// Sign-out is wired centrally in shell.js (doLogout + a delegated click
|
|
932
449
|
// listener on the sign-out anchors), so it runs the real POST -> cookie-clear
|
|
933
450
|
// -> bounce-home on EVERY hall page, not only this index. It used to live
|
|
@@ -1032,8 +549,8 @@
|
|
|
1032
549
|
// reads what this page holds NOW rather than what it held at boot. Everything
|
|
1033
550
|
// else is stable for the life of the page. This call sits AFTER every
|
|
1034
551
|
// declaration it passes — moving it earlier is a temporal-dead-zone crash.
|
|
1035
|
-
const {
|
|
1036
|
-
API, RANK_ORDER, api, brandWorld, curLabel,
|
|
552
|
+
const { lbState, mountHallWidgets, renderGreeting, renderLanding, renderSessionRefused, renderNav, renderProfile, HOME_SHIPS_LIMIT, setRecentShips } = window.OTBHallRender({
|
|
553
|
+
API, RANK_ORDER, api, brandWorld, curLabel, normalizeRank, postJSON, rankLabel, rankWeight, toast,
|
|
1037
554
|
get lastCostState() { return lastCostState; },
|
|
1038
555
|
get lastKarmaState() { return lastKarmaState; },
|
|
1039
556
|
get lastMeState() { return lastMeState; },
|
|
@@ -105,10 +105,10 @@
|
|
|
105
105
|
</div>
|
|
106
106
|
</div>
|
|
107
107
|
|
|
108
|
-
<script src="/builders/dom-utils.js?v=2026-
|
|
109
|
-
<script src="/builders/shell-trail.js?v=2026-
|
|
110
|
-
<script src="/builders/craft-mode.js?v=2026-11-05-
|
|
111
|
-
<script src="/builders/shell.js?v=2026-11-05-
|
|
108
|
+
<script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
|
|
109
|
+
<script src="/builders/shell-trail.js?v=2026-11-05-numbers" defer></script>
|
|
110
|
+
<script src="/builders/craft-mode.js?v=2026-11-05-numbers" defer></script>
|
|
111
|
+
<script src="/builders/shell.js?v=2026-11-05-numbers" defer></script>
|
|
112
112
|
<script src="/builders/palette.js" defer></script>
|
|
113
113
|
<script src="/builders/gov-living.js?v=2026-10-09-positions1" defer></script>
|
|
114
114
|
<script src="/builders/gov-feeds.js?v=2026-10-09-living3" defer></script>
|
|
@@ -159,10 +159,10 @@
|
|
|
159
159
|
|
|
160
160
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
161
161
|
|
|
162
|
-
<script src="/builders/dom-utils.js?v=2026-
|
|
163
|
-
<script src="/builders/shell-trail.js?v=2026-
|
|
164
|
-
<script src="/builders/craft-mode.js?v=2026-11-05-
|
|
165
|
-
<script src="/builders/shell.js?v=2026-11-05-
|
|
162
|
+
<script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
|
|
163
|
+
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
164
|
+
<script src="/builders/craft-mode.js?v=2026-11-05-numbers"></script>
|
|
165
|
+
<script src="/builders/shell.js?v=2026-11-05-numbers"></script>
|
|
166
166
|
<script src="/builders/palette.js" defer></script>
|
|
167
167
|
<script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
|
|
168
168
|
<script src="/builders/collab.js?v=2026-09-24-archive"></script>
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
|
|
52
52
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
53
53
|
|
|
54
|
-
<script src="/builders/dom-utils.js?v=2026-
|
|
55
|
-
<script src="/builders/shell-trail.js?v=2026-
|
|
56
|
-
<script src="/builders/craft-mode.js?v=2026-11-05-
|
|
57
|
-
<script src="/builders/shell.js?v=2026-11-05-
|
|
54
|
+
<script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
|
|
55
|
+
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
56
|
+
<script src="/builders/craft-mode.js?v=2026-11-05-numbers"></script>
|
|
57
|
+
<script src="/builders/shell.js?v=2026-11-05-numbers"></script>
|
|
58
58
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
59
59
|
<script src="/builders/palette.js" defer></script>
|
|
60
60
|
<script src="/builders/hall-kit.js"></script>
|