@bongos/core 1.24.41 → 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 +117 -92
- package/docs/copy-inventory.md +98 -137
- package/docs/copy-registry.json +244 -644
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/primer.md +1 -1
- package/docs/page-inventory.json +3 -3
- package/docs/page-readings.json +494 -535
- 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 +35 -763
- 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 +25 -771
- package/modules/hall-ui/public/hall-widgets.js +14 -123
- 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 +8 -22
- 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/setup-gate.js +156 -0
- package/modules/hall-ui/public/shell-trail.js +2 -1
- package/modules/hall-ui/public/shell.js +44 -26
- package/modules/hall-ui/public/street.html +4 -4
- package/modules/hall-ui/public/studio.html +4 -4
- package/modules/hall-ui/public/style.css +85 -117
- 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/modules/hall-ui/records/setup-gate.md +43 -0
- package/modules/lifecycle/routes/tasks.js +2 -2
- package/modules/onboarding/onboarding-state.js +41 -20
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/routes/me.js +2 -2
- 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 +41 -27
- 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_landing_boot.mjs +2 -3
- 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_onboarding_bongos_setup.mjs +14 -14
- 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 +10 -6
- package/tests/nav_permission_atoms.mjs +4 -2
- package/tests/onboarding_state.mjs +33 -0
- package/tests/page_tweak_artist.mjs +2 -1
- package/tests/rank_tier_single_source.mjs +3 -0
- package/tests/setup_flow.mjs +242 -0
- package/tests/task_visuals.mjs +3 -1
- package/tests/welcome_boot.mjs +3 -2
|
@@ -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,36 +93,17 @@
|
|
|
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
|
-
// ---- repo info (the clone URL the
|
|
99
|
+
// ---- repo info (the clone URL the setup screen hands a newcomer) ---
|
|
130
100
|
//
|
|
131
101
|
// The /builder-* commands live INSIDE the repo (.claude/skills/, shelling out
|
|
132
102
|
// to scripts/bongos/*.js). The old repo-clone walkthrough (and the
|
|
133
103
|
// /public/repo-info fetch that fed it) was removed with the setup-first
|
|
134
|
-
// redesign (#777); the
|
|
135
|
-
// repo coords instead. The /public/repo-info surface
|
|
136
|
-
// callers.
|
|
104
|
+
// redesign (#777); the setup screen's clone block (setup-gate.js, task 1005076)
|
|
105
|
+
// reads the instance's repo coords instead. The /public/repo-info surface
|
|
106
|
+
// still exists for other callers.
|
|
137
107
|
|
|
138
108
|
// ---- rank badge (V3.R16 #240) -------------------------------------------
|
|
139
109
|
// Three live ranks per ADR 0018: archon > metic > xenos. Missing rank
|
|
@@ -149,7 +119,7 @@
|
|
|
149
119
|
// xenos — #692 / ADR 0034) to the entry rank. Reserved values still on the
|
|
150
120
|
// citizen/divine ladder would otherwise render as a dashed "unknown" plaque
|
|
151
121
|
// while the hall elsewhere shows them as xenos. Applied to the
|
|
152
|
-
// builder-facing standing surfaces — the
|
|
122
|
+
// builder-facing standing surfaces — the profile Standing
|
|
153
123
|
// badge, and the public Roll — so they agree. NOT applied to the Archon-only
|
|
154
124
|
// roster below, which intentionally shows the true stored rank so an Archon
|
|
155
125
|
// can spot and fix a stray value. hall-render.js's "Where you stand" card
|
|
@@ -187,27 +157,10 @@
|
|
|
187
157
|
// GATE new claims and whose reward is pending until they land on ship.
|
|
188
158
|
let lastRebaseState = null;
|
|
189
159
|
|
|
190
|
-
// Cached /me payload from the most recent loadAll() call
|
|
191
|
-
//
|
|
192
|
-
// from cached state without a fresh /me round-trip. Distinct from
|
|
193
|
-
// lastKarmaState: that one only stashes karma; this stashes the whole
|
|
194
|
-
// payload so renderWelcome's builder + onboarding args are available.
|
|
160
|
+
// Cached /me payload from the most recent loadAll() call, for re-renders
|
|
161
|
+
// that need the builder without a fresh /me round-trip.
|
|
195
162
|
let lastMeState = null;
|
|
196
163
|
|
|
197
|
-
// Instance repo coords for the "get the code" clone block. The public instance
|
|
198
|
-
// manifest (GET /api/bongos/instance) is the channel for cross-instance repo coords
|
|
199
|
-
// — clientBranding deliberately omits `repo`, so window.__BRANDING__ can't carry
|
|
200
|
-
// it. Cached; the welcome re-renders when it lands so a fresh instance shows ITS
|
|
201
|
-
// OWN repo, never a hardcoded one (ADR 0062 §3, task 1705).
|
|
202
|
-
let instanceRepo = null;
|
|
203
|
-
async function loadInstanceRepo() {
|
|
204
|
-
try {
|
|
205
|
-
const inst = await getJSON(`${API}/instance`, { softAuth: true });
|
|
206
|
-
instanceRepo = inst && inst.repo && !inst.repo.error ? inst.repo : null;
|
|
207
|
-
if (instanceRepo && lastMeState) renderWelcome(lastMeState.builder, lastMeState.onboarding || null);
|
|
208
|
-
} catch (_) { /* best-effort — the clone block falls back to generic guidance */ }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
164
|
// ---- hall section nav (#408) --------------------------------------------
|
|
212
165
|
//
|
|
213
166
|
// Single source of truth for what appears in the sticky strip at top of
|
|
@@ -218,9 +171,6 @@
|
|
|
218
171
|
//
|
|
219
172
|
// Filter rules at render time:
|
|
220
173
|
// archonOnly: true → skip unless /me returns rank=archon.
|
|
221
|
-
// welcomeChip:true → ALWAYS shown pre-graduation (even when the
|
|
222
|
-
// panel is dismissed) so the builder can re-open
|
|
223
|
-
// it. Hidden post-graduation just like the panel.
|
|
224
174
|
// default → skip if the section's element is [hidden].
|
|
225
175
|
//
|
|
226
176
|
// Adding a new section: add a row here AND give the <section> a stable
|
|
@@ -242,31 +192,17 @@
|
|
|
242
192
|
// per-widget gates (archonOnly) carried on the registry entry.
|
|
243
193
|
|
|
244
194
|
// task 1723: which container a widget mounts into. A widget may declare
|
|
245
|
-
// `zone` (a fine-grained Home slot)
|
|
246
|
-
//
|
|
247
|
-
// 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
|
|
248
198
|
// container lives INSIDE <main>, so the `main .scroll` sweeps (newcomer gate,
|
|
249
199
|
// landing hide-all) keep working.
|
|
250
200
|
|
|
251
|
-
// task 2174: Standing is an identity band + four rail sections grouped by the
|
|
252
|
-
// question they answer. This array is the rail's order and its labels; each
|
|
253
|
-
// entry gets a #standing-<key> anchor container inside #zone-standing that the
|
|
254
|
-
// group's widgets mount into. The Profile card declares no group, so it stays
|
|
255
|
-
// above them as the band.
|
|
256
|
-
|
|
257
|
-
// Create ALL four group containers the first time any group is asked for, so
|
|
258
|
-
// the rail's order is STANDING_GROUPS' — not whichever group's first widget
|
|
259
|
-
// happens to register earliest. Creating them on demand (rather than up front)
|
|
260
|
-
// is what keeps the Profile band on top: an ungrouped Standing widget appends
|
|
261
|
-
// straight to #zone-standing, so anything registered before the first grouped
|
|
262
|
-
// widget lands above the groups. Idempotent.
|
|
263
|
-
|
|
264
|
-
|
|
265
201
|
// mountHallWidgets — stamp every registered widget's <section> into its view
|
|
266
202
|
// container (falling back to <main>), in registry order, then render its
|
|
267
203
|
// skeleton via mount(host). Idempotent: a second call (defensive) re-uses an
|
|
268
204
|
// existing <section>. Runs once at bootstrap BEFORE wireEvents() so the static
|
|
269
|
-
// element ids the data-render + event-wiring code expects (#
|
|
205
|
+
// element ids the data-render + event-wiring code expects (#acclaim-feed,
|
|
270
206
|
// #recent-ships, #lb-prev, …) are present. Each widget mount is
|
|
271
207
|
// failure-isolated so one bad widget can't blank the hall.
|
|
272
208
|
// task 2169: zone budgets — Home cannot grow unbounded again. Caps count
|
|
@@ -277,17 +213,9 @@
|
|
|
277
213
|
// The budgets themselves become a CI fitness assertion in task 2177.
|
|
278
214
|
|
|
279
215
|
|
|
280
|
-
//
|
|
281
|
-
// the
|
|
282
|
-
//
|
|
283
|
-
// so the same instance survives re-renders.
|
|
284
|
-
// 2. the Home re-open affordance for a dismissed welcome banner (the old
|
|
285
|
-
// welcome chip's only job);
|
|
286
|
-
// 3. the Standing view's in-page section rail (#standing-rail) — task 2174:
|
|
287
|
-
// one entry per GROUP that currently has something to show, not one per
|
|
288
|
-
// widget. A group with every member hidden (a newcomer, an empty karma
|
|
289
|
-
// feed) hides itself and drops off the rail.
|
|
290
|
-
// 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.
|
|
291
219
|
|
|
292
220
|
// #748: newcomer gate. A Xenos sees a deliberately spare hall — their
|
|
293
221
|
// onboarding path and their Standing card — until they graduate to Thetes.
|
|
@@ -309,7 +237,7 @@
|
|
|
309
237
|
// (which un-hide their own scroll on data) respect it; the section-hiding
|
|
310
238
|
// sweep then runs before renderNav so the chip strip matches.
|
|
311
239
|
const NEWCOMER_VISIBLE_SECTIONS = new Set([
|
|
312
|
-
'
|
|
240
|
+
'profile-scroll',
|
|
313
241
|
]);
|
|
314
242
|
let newcomerGated = false;
|
|
315
243
|
|
|
@@ -325,21 +253,6 @@
|
|
|
325
253
|
// gates. This function keeps only the widget-section sweep.
|
|
326
254
|
}
|
|
327
255
|
|
|
328
|
-
function reopenWelcomeFromNav(e) {
|
|
329
|
-
e.preventDefault();
|
|
330
|
-
const id = window.__buildersSelfId;
|
|
331
|
-
if (!id || !lastMeState) return;
|
|
332
|
-
try {
|
|
333
|
-
window.localStorage.removeItem(welcomeDismissalKey(id));
|
|
334
|
-
} catch (_) { /* private mode — re-open in-memory only */ }
|
|
335
|
-
// Re-render welcome from cached /me state and refresh the nav so the
|
|
336
|
-
// chip drops its "dismissed" styling.
|
|
337
|
-
renderWelcome(lastMeState.builder, lastMeState.onboarding || null);
|
|
338
|
-
renderNav(lastMeState.builder, lastMeState.onboarding || null);
|
|
339
|
-
// Now scroll to the (newly-unhidden) welcome panel.
|
|
340
|
-
const target = document.getElementById('welcome-scroll');
|
|
341
|
-
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
342
|
-
}
|
|
343
256
|
|
|
344
257
|
|
|
345
258
|
// Wires the public landing's access-request form. Submits to the one public
|
|
@@ -353,445 +266,9 @@
|
|
|
353
266
|
// renderDiscordLink / unlinkDiscord (F7, ADR 0033) moved to
|
|
354
267
|
// public-builders/settings.js — these controls now live on /builders/settings.
|
|
355
268
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
.map((el) => el.value);
|
|
360
|
-
const btn = $('#disc-save');
|
|
361
|
-
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
|
|
362
|
-
try {
|
|
363
|
-
const resp = await patchJSON(`${API}/me/disciplines`, { preferred_disciplines: selected });
|
|
364
|
-
renderProfile(resp.builder);
|
|
365
|
-
toast(selected.length ? `Preferred work: ${selected.join(', ')}` : 'Preference cleared.');
|
|
366
|
-
} catch (err) {
|
|
367
|
-
if (err && err.message === 'redirecting to auth') return;
|
|
368
|
-
console.error('[builders] save disciplines failed', err);
|
|
369
|
-
toast('Could not save your preference. Try again.');
|
|
370
|
-
if (btn) { btn.disabled = false; btn.textContent = 'Save'; }
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ---- first-visit welcome panel (V3.R77 #296) ----------------------------
|
|
375
|
-
// Greets a not-yet-graduated builder with their rank, what their rank
|
|
376
|
-
// unlocks, the next checklist step, and the two doors into the hall: the
|
|
377
|
-
// orientation maps and the /builder-start command. Dismissable. Reappears
|
|
378
|
-
// if the builder's onboarding stage regresses (rare — e.g. a stage row
|
|
379
|
-
// wipe in dev or an Archon-driven reset).
|
|
380
|
-
//
|
|
381
|
-
// Persistence: localStorage key `builders:welcome-dismissed:<builder_id>`
|
|
382
|
-
// stores the stage *index* (0-6) at the moment of dismissal. On every
|
|
383
|
-
// render we compute the builder's current stage index; if it's strictly
|
|
384
|
-
// less than the dismissed-at index, the panel reappears. Per-builder key
|
|
385
|
-
// so a shared browser doesn't carry one builder's dismissal to another.
|
|
386
|
-
//
|
|
387
|
-
// The panel only renders for the LOGGED-IN viewer (uses /me). It is not
|
|
388
|
-
// shown on profile pages of other builders — that's the read-only
|
|
389
|
-
// onboarding-list panel below, which is roster-visible.
|
|
390
|
-
|
|
391
|
-
// Canonical order of the onboarding stages, mirroring
|
|
392
|
-
// src/bongos/onboarding-state.js STAGES. 'graduated' is the terminal stage;
|
|
393
|
-
// when current === 'graduated' the panel hides entirely.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
// task 2169 (banner budget): exactly one banner panel at a time. The
|
|
398
|
-
// checklist render runs before the welcome render in the boot pass, so after
|
|
399
|
-
// the welcome panel's visibility settles we re-evaluate the checklist —
|
|
400
|
-
// welcome visible → checklist waits; welcome dismissed/hidden → checklist
|
|
401
|
-
// takes the banner slot (renderOnboarding carries the mirror check).
|
|
402
|
-
function renderWelcome(builder, onboarding) {
|
|
403
|
-
renderWelcomeCore(builder, onboarding);
|
|
404
|
-
renderOnboarding(
|
|
405
|
-
onboarding || (lastMeState && lastMeState.onboarding) || null,
|
|
406
|
-
lastMeState && lastMeState.discord
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// ---- onboarding checklist (V3.R75 #294; reshaped #777) ------------------
|
|
412
|
-
// Renders the "Path into the City" checklist for the CURRENT logged-in
|
|
413
|
-
// builder. Hidden once graduated (current_stage === 'graduated') so it
|
|
414
|
-
// doesn't clutter the hall for full citizens. The displayed list (#777) is a
|
|
415
|
-
// short, action-only orientation path: Join Discord → Choose craft → Run
|
|
416
|
-
// /builder-setup → Ship three (→ Thetes). Every step AUTO-detects server-side
|
|
417
|
-
// (#777, per Lars: no manual ticks). Only two carry a control:
|
|
418
|
-
// * pick_disciplines — the craft picker (PATCH /me/disciplines).
|
|
419
|
-
// * join_discord — a Connect-Discord button (the existing OAuth flow,
|
|
420
|
-
// auto-joins + assigns rank role); clears when linked.
|
|
421
|
-
// cli_setup clears once the Bongos CLI is authenticated (db.hasCliSession) and
|
|
422
|
-
// ship_three on the 3rd ship — both passive. The terminal ship_three step
|
|
423
|
-
// shows the live "N of 3" counter (its `detail`). `discord` is the /me discord
|
|
424
|
-
// block ({ linked, configured }) — when Discord isn't configured server-side
|
|
425
|
-
// the Connect button is suppressed.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// V3 (2026-05-30): clear the pick_disciplines onboarding stage from the path
|
|
429
|
-
// checklist. Saves the chosen craft via the same PATCH /me/disciplines route
|
|
430
|
-
// the profile uses, then re-renders the checklist (and the profile picker) so
|
|
431
|
-
// both reflect the choice. The route echoes the updated onboarding state.
|
|
432
|
-
async function pickDiscipline(value) {
|
|
433
|
-
// Multi-select toggle: add the craft if not yet chosen, remove it if it is,
|
|
434
|
-
// then save the full set (any mix of engineer, artist, ideator).
|
|
435
|
-
const current = (lastMeState && lastMeState.builder && Array.isArray(lastMeState.builder.preferred_disciplines))
|
|
436
|
-
? lastMeState.builder.preferred_disciplines : [];
|
|
437
|
-
const selected = current.includes(value)
|
|
438
|
-
? current.filter((d) => d !== value)
|
|
439
|
-
: [...current, value];
|
|
440
|
-
const buttons = document.querySelectorAll('.onboarding-item__disc');
|
|
441
|
-
buttons.forEach((b) => { b.disabled = true; });
|
|
442
|
-
try {
|
|
443
|
-
const resp = await patchJSON(`${API}/me/disciplines`, { preferred_disciplines: selected });
|
|
444
|
-
if (resp.builder && lastMeState) {
|
|
445
|
-
// Keep the cached /me state and the profile picker in sync.
|
|
446
|
-
lastMeState.builder = resp.builder;
|
|
447
|
-
lastMeState.onboarding = resp.onboarding || lastMeState.onboarding;
|
|
448
|
-
}
|
|
449
|
-
if (resp.onboarding) renderOnboarding(resp.onboarding, lastMeState && lastMeState.discord);
|
|
450
|
-
if (resp.builder) renderProfile(resp.builder);
|
|
451
|
-
const axis = window.OTB.roleAxisLabel();
|
|
452
|
-
toast(selected.length ? `${axis}: ${selected.map((d) => window.OTB.roleLabel(d)).join(', ')}.` : `${axis} cleared.`);
|
|
453
|
-
} catch (err) {
|
|
454
|
-
if (err && err.message === 'redirecting to auth') return;
|
|
455
|
-
console.error('[builders] pick discipline failed', err);
|
|
456
|
-
toast('Could not save just now. Try again.');
|
|
457
|
-
buttons.forEach((b) => { b.disabled = false; });
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// ---- achievements --------------------------------------------------------
|
|
462
|
-
|
|
463
|
-
function renderAchievements(all, unlocked, lockedWithProgress) {
|
|
464
|
-
const unlockedById = new Map(unlocked.map((u) => [u.achievement_id, u]));
|
|
465
|
-
// Map achievement_id → progress_text for locked entries. Falsy/empty
|
|
466
|
-
// progress just shows the credit-bonus line as before.
|
|
467
|
-
const progressById = new Map(
|
|
468
|
-
(lockedWithProgress || []).map((l) => [l.achievement_id, l.progress_text || '']),
|
|
469
|
-
);
|
|
470
|
-
const grid = $('#ach-grid');
|
|
471
|
-
if (!all.length) {
|
|
472
|
-
grid.innerHTML = `<div class="profile__placeholder">No achievements earned yet.</div>`;
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
grid.innerHTML = all.map((a) => {
|
|
476
|
-
const got = unlockedById.get(a.id);
|
|
477
|
-
const cls = got ? 'ach' : 'ach ach--locked';
|
|
478
|
-
const bonus = a.credit_bonus ? `<p class="ach__bonus">+${fmtNum(a.credit_bonus)} ${(window.OTBBranding?.currencyLabel?.() ?? 'credits')}</p>` : '';
|
|
479
|
-
const when = got ? `<p class="ach__bonus">Won ${fmtDate(got.unlocked_at)}</p>` : '';
|
|
480
|
-
const progress = !got && progressById.has(a.id) && progressById.get(a.id)
|
|
481
|
-
? `<p class="ach__progress">${escapeHtml(progressById.get(a.id))}</p>`
|
|
482
|
-
: '';
|
|
483
|
-
return `
|
|
484
|
-
<div class="${cls}" title="${escapeHtml(a.id)}">
|
|
485
|
-
<div class="ach__icon" aria-hidden="true">${escapeHtml(a.icon || '✦')}</div>
|
|
486
|
-
<div class="ach__body">
|
|
487
|
-
<p class="ach__name">${escapeHtml(a.name)}</p>
|
|
488
|
-
<p class="ach__desc">${escapeHtml(a.description || '')}</p>
|
|
489
|
-
${when || bonus}
|
|
490
|
-
${progress}
|
|
491
|
-
</div>
|
|
492
|
-
</div>
|
|
493
|
-
`;
|
|
494
|
-
}).join('');
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// ---- recent ships --------------------------------------------------------
|
|
498
|
-
|
|
499
|
-
// All shipped works for this builder, fetched once via ?limit=all then
|
|
500
|
-
// filtered, sorted, and paged entirely client-side. Mirrors the status site's
|
|
501
|
-
// Lately Inscribed pattern (modules/status-ui/public/status.js renderShippedPage), with
|
|
502
|
-
// an additional version filter since a builder's history spans versions.
|
|
503
|
-
// task 2174: paging moved to the OTBKit pager (per-page select + persistence);
|
|
504
|
-
// 10 stays the default page size this table has always used.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// task 1957 — a shipped row's payout is the ACTUAL ledger figure (credits_awarded),
|
|
508
|
-
// not the creation-time estimate. taskCreditValue picks the right one by status.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
// Re-render when filters change. Reset to page 1 so the new filter starts at
|
|
513
|
-
// the top — otherwise switching to a smaller window can leave you on page 9
|
|
514
|
-
// of a 2-page result.
|
|
515
|
-
function onShipsFilterChange() {
|
|
516
|
-
shipsPager.page = 1;
|
|
517
|
-
renderRecentShips();
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// ---- recent grades (task 197) -------------------------------------------
|
|
521
|
-
|
|
522
|
-
function renderRecentGrades(grades) {
|
|
523
|
-
const tbody = $('#recent-grades tbody');
|
|
524
|
-
if (!tbody) return;
|
|
525
|
-
if (!grades || !grades.length) {
|
|
526
|
-
tbody.innerHTML = emptyRow(5, 'No work reviewed yet.');
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
tbody.innerHTML = grades.map((g) => {
|
|
530
|
-
// A grader OUTAGE row (panel_outcome 'unavailable') carries a placeholder
|
|
531
|
-
// score of 0 — label it, never render it as a 0.0 fail (task 1002652).
|
|
532
|
-
const isOutage = g.panel_outcome === 'unavailable';
|
|
533
|
-
// A json-null gating axis (task 1003318 — the Quality worker never ran)
|
|
534
|
-
// is likewise not a judgment: the score is the surviving axis, not a fail.
|
|
535
|
-
const isLensOut = !isOutage && !!g.rubric_scores && g.rubric_scores.code_quality === null;
|
|
536
|
-
const score = isOutage || isLensOut ? '—' : Number(g.score).toFixed(1);
|
|
537
|
-
// A verdict is verification state: it wears the fixed vocabulary through
|
|
538
|
-
// .ledger__verdict--* (style.css), never an inline colour. The literals
|
|
539
|
-
// that stood here painted `passed` from a --olive that is not a token of
|
|
540
|
-
// the world, so every hall ever rendered it as the fallback (task 1003495).
|
|
541
|
-
const verdict = isOutage
|
|
542
|
-
? `<span class="ledger__verdict--none">not graded (grader outage)</span>`
|
|
543
|
-
: isLensOut
|
|
544
|
-
? `<span class="ledger__verdict--none">not graded (quality lens unavailable)</span>`
|
|
545
|
-
: g.passed
|
|
546
|
-
? `<span class="ledger__verdict--ok">passed</span>`
|
|
547
|
-
: `<span class="ledger__verdict--danger">fell short</span>`;
|
|
548
|
-
return `
|
|
549
|
-
<tr>
|
|
550
|
-
<td class="ledger__num">#${g.task_id}</td>
|
|
551
|
-
<td>${escapeHtml(g.title || '')}</td>
|
|
552
|
-
<td class="ledger__drachmae">${isOutage || isLensOut ? '—' : `${score} / 10`}</td>
|
|
553
|
-
<td>${verdict}</td>
|
|
554
|
-
<td class="ledger__date">${fmtDate(g.graded_at)}</td>
|
|
555
|
-
</tr>
|
|
556
|
-
`;
|
|
557
|
-
}).join('');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function renderRollingNote(rolling) {
|
|
561
|
-
const note = $('#grades-rolling-note');
|
|
562
|
-
if (!note) return;
|
|
563
|
-
if (!rolling || !rolling.n || Number(rolling.n) === 0) {
|
|
564
|
-
note.hidden = true;
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const avg = Number(rolling.avg_score).toFixed(1);
|
|
568
|
-
const passed = Number(rolling.n_passed || 0);
|
|
569
|
-
const n = Number(rolling.n);
|
|
570
|
-
note.textContent = `Last 14 days: ${avg} / 10 average across ${n} graded work${n === 1 ? '' : 's'} (${passed} passed).`;
|
|
571
|
-
note.hidden = false;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// ---- the dev curve (V3.R72 #291, C5) ------------------------------------
|
|
575
|
-
//
|
|
576
|
-
// Per-builder historical analytics: ship rate, cost-per-task, duration, and
|
|
577
|
-
// grade pass-rate over time, plus a top-kinds fingerprint. Charts are inline
|
|
578
|
-
// SVG sparklines — no chart library (the task spec's explicit constraint).
|
|
579
|
-
// An Archon additionally gets a "You / All builders" scope toggle backed by
|
|
580
|
-
// the ecosystem aggregate (individuals stripped), satisfying the
|
|
581
|
-
// cross-builder view requirement without a per-builder roster on this page.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// Build a tiny SVG line sparkline from an array of numeric y-values. Returns
|
|
585
|
-
// a self-contained <svg> string. A flat/short series degrades to a baseline.
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const pct = (v) => (v == null ? '—' : `${Math.round(Number(v) * 100)}%`);
|
|
590
|
-
const usd = (v) => `$${(Number(v) || 0).toFixed(2)}`;
|
|
591
|
-
const mins = (v) => (v == null ? '—' : `${Math.round(Number(v))}m`);
|
|
592
|
-
|
|
593
|
-
// Render the curve for whichever scope is active. `data` is either the self
|
|
594
|
-
// payload (/analytics/builder/:id) or the ecosystem payload, normalized below.
|
|
595
|
-
|
|
596
|
-
// Fire-and-forget loader: fetch the builder's own curve (and, for an Archon,
|
|
597
|
-
// the ecosystem aggregate) and render. A failed/empty feed simply leaves the
|
|
598
|
-
// section hidden rather than holding up the rest of the hall.
|
|
599
|
-
async function loadAnalytics(builder) {
|
|
600
|
-
const scroll = $('#analytics-scroll');
|
|
601
|
-
if (!scroll || !builder?.id) return;
|
|
602
|
-
analyticsState.builderId = builder.id;
|
|
603
|
-
analyticsState.scope = 'self';
|
|
604
|
-
analyticsState.kind = null;
|
|
605
|
-
try {
|
|
606
|
-
const self = await getJSON(`${API}/analytics/builder/${builder.id}?days=90`);
|
|
607
|
-
analyticsState.self = self;
|
|
608
|
-
// The picker's options come from THIS first, unfiltered load and are then
|
|
609
|
-
// left alone — see kindFilterHtml. A later filtered payload's top_kinds is
|
|
610
|
-
// deliberately not allowed to overwrite them.
|
|
611
|
-
analyticsState.kinds = Array.isArray(self?.top_kinds) ? self.top_kinds : [];
|
|
612
|
-
// A gated newcomer with nothing shipped: keep the section hidden.
|
|
613
|
-
if (!self || (self.headline?.total_shipped || 0) === 0) { scroll.hidden = true; return; }
|
|
614
|
-
scroll.hidden = false;
|
|
615
|
-
renderCurveBody();
|
|
616
|
-
renderNav(builder, lastMeState?.onboarding || null); // re-show the Curve chip now that the section is visible
|
|
617
|
-
// THE BENCHMARK IS FOR EVERY BUILDER, NOT JUST THE ARCHON (task 1004005).
|
|
618
|
-
// This fetch used to sit behind `if (analyticsState.isArchon)`, so the
|
|
619
|
-
// ecosystem average a builder is meant to measure themselves against was
|
|
620
|
-
// fetched for exactly one of them. The route is `rank: public` — no auth
|
|
621
|
-
// middleware, CORS '*', per-builder identity stripped server-side — and the
|
|
622
|
-
// unauthenticated status page already reads it, so the gate protected
|
|
623
|
-
// nothing and only withheld the comparison. Still fire-and-forget: the
|
|
624
|
-
// ecosystem feed is optional context, never a reason to hold up the curve.
|
|
625
|
-
getJSON(`${API}/public/analytics/ecosystem?days=90`)
|
|
626
|
-
// ALWAYS re-render, not only in the 'eco' scope. That condition was
|
|
627
|
-
// correct while the two scopes merely SWAPPED which single series was
|
|
628
|
-
// drawn; now the ecosystem series is also the benchmark overlaid on the
|
|
629
|
-
// 'self' view, so a self-scoped reader who arrived before this resolved
|
|
630
|
-
// would sit with no overlay until they touched an unrelated control.
|
|
631
|
-
.then((eco) => { analyticsState.eco = eco; renderCurveBody(); })
|
|
632
|
-
.catch(() => { /* ecosystem optional */ });
|
|
633
|
-
} catch (err) {
|
|
634
|
-
console.error('[builders] analytics load failed', err);
|
|
635
|
-
scroll.hidden = true;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Re-fetch BOTH series under a kind filter (task 1004005). Both, always: an
|
|
640
|
-
// overlay that narrowed one side and not the other would compare one kind of
|
|
641
|
-
// work against all work and still look like a like-for-like benchmark — the
|
|
642
|
-
// failure this task's own text calls worse than no benchmark at all.
|
|
643
|
-
//
|
|
644
|
-
// `kinds` is deliberately not refreshed here; see kindFilterHtml.
|
|
645
|
-
let curveKindSeq = 0;
|
|
646
|
-
async function reloadCurveForKind(kind) {
|
|
647
|
-
const id = analyticsState.builderId;
|
|
648
|
-
if (!id) return;
|
|
649
|
-
analyticsState.kind = kind;
|
|
650
|
-
// A dropdown can be changed faster than a round trip returns, and the two
|
|
651
|
-
// fetches can land out of order. Only the newest request may write state.
|
|
652
|
-
const seq = ++curveKindSeq;
|
|
653
|
-
const q = kind ? `&kind=${encodeURIComponent(kind)}` : '';
|
|
654
|
-
renderCurveBody(); // reflect the new selection immediately
|
|
655
|
-
try {
|
|
656
|
-
const [self, eco] = await Promise.all([
|
|
657
|
-
getJSON(`${API}/analytics/builder/${id}?days=90${q}`),
|
|
658
|
-
getJSON(`${API}/public/analytics/ecosystem?days=90${q}`).catch(() => null),
|
|
659
|
-
]);
|
|
660
|
-
if (seq !== curveKindSeq) return;
|
|
661
|
-
analyticsState.self = self;
|
|
662
|
-
if (eco) analyticsState.eco = eco;
|
|
663
|
-
renderCurveBody();
|
|
664
|
-
} catch (err) {
|
|
665
|
-
if (seq !== curveKindSeq) return;
|
|
666
|
-
console.error('[builders] curve kind filter failed', err);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// ---- karma feed (V3.R26 #246) -------------------------------------------
|
|
671
|
-
//
|
|
672
|
-
// The recent karma_log entries for the current builder. The /me payload
|
|
673
|
-
// already includes them under `karma.recent`. The scroll stays hidden when
|
|
674
|
-
// the builder has no history yet (zero karma is a common starting state,
|
|
675
|
-
// so we don't want to push an empty table at every new citizen).
|
|
676
|
-
|
|
677
|
-
function renderKarmaFeed(entries) {
|
|
678
|
-
const scroll = $('#karma-scroll');
|
|
679
|
-
const tbody = $('#karma-feed tbody');
|
|
680
|
-
if (!scroll || !tbody) return;
|
|
681
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
682
|
-
// Hide the scroll entirely if the builder has no karma history.
|
|
683
|
-
// Keeps the page tidy for fresh citizens; reappears the moment a
|
|
684
|
-
// karma_log row lands.
|
|
685
|
-
scroll.hidden = true;
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
// #748: a gated newcomer never shows the karma feed, even if karma exists.
|
|
689
|
-
scroll.hidden = newcomerGated;
|
|
690
|
-
tbody.innerHTML = entries.map((k) => {
|
|
691
|
-
const delta = Number(k.delta) || 0;
|
|
692
|
-
const deltaCls = signClasses(delta, 'karma-feed__delta');
|
|
693
|
-
const deltaText = delta > 0 ? `+${fmtNum(delta)}` : fmtNum(delta);
|
|
694
|
-
const granted = k.granted_by
|
|
695
|
-
? `@${escapeHtml(k.granted_by_login || k.granted_by_display_name || `builder #${k.granted_by}`)}`
|
|
696
|
-
: '<span style="opacity:0.6;">— all builders —</span>';
|
|
697
|
-
return `
|
|
698
|
-
<tr>
|
|
699
|
-
<td class="ledger__date">${escapeHtml(fmtDate(k.recorded_at))}</td>
|
|
700
|
-
<td class="karma-feed__delta ${deltaCls}">${deltaText}</td>
|
|
701
|
-
<td>${escapeHtml(k.reason || '')}</td>
|
|
702
|
-
<td>${granted}</td>
|
|
703
|
-
</tr>
|
|
704
|
-
`;
|
|
705
|
-
}).join('');
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// ---- peer acclaim feed (V3.R25 #245) ------------------------------------
|
|
709
|
-
//
|
|
710
|
-
// Other builders' recently confirmed/shipped work, each with a +/- vote
|
|
711
|
-
// widget. Votes fold into the shipping builder's karma on a weekly tally,
|
|
712
|
-
// lagged 7 days (server-enforced). Metic+ may vote; the endpoint returns
|
|
713
|
-
// can_vote so Xenos see the tallies read-only. The endpoint already excludes
|
|
714
|
-
// the caller's own ships (anti-self-vote), so there's nothing to vote on that
|
|
715
|
-
// isn't a peer's work.
|
|
716
|
-
|
|
717
|
-
let acclaimCanVote = false;
|
|
718
|
-
// task 1002446: this was the hall's last unpaged list. The endpoint returns the
|
|
719
|
-
// whole feed in one call, so the rows are held here and the kit's client-side
|
|
720
|
-
// pager slices them — a page change re-renders without a refetch.
|
|
721
|
-
const acclaimState = { tasks: [] };
|
|
722
|
-
const acclaimPager = window.OTBKit.makePager('acclaim', 'otb.standing.acclaimPerPage', { defaultPerPage: 10 });
|
|
723
|
-
|
|
724
|
-
function renderAcclaimFeed(data) {
|
|
725
|
-
const scroll = $('#acclaim-scroll');
|
|
726
|
-
if (!scroll) return;
|
|
727
|
-
const tasks = (data && Array.isArray(data.tasks)) ? data.tasks : [];
|
|
728
|
-
acclaimState.tasks = tasks;
|
|
729
|
-
acclaimCanVote = !!(data && data.can_vote);
|
|
730
|
-
if (tasks.length === 0) {
|
|
731
|
-
// Nothing to acclaim yet (e.g. the only confirmed work so far is your
|
|
732
|
-
// own). Hide rather than show an empty ledger.
|
|
733
|
-
scroll.hidden = true;
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
// #748: a gated newcomer never shows the acclaim feed.
|
|
737
|
-
scroll.hidden = newcomerGated;
|
|
738
|
-
// task 2174: this feed lands after loadAll built the rail, and it can be the
|
|
739
|
-
// only visible member of the Karma group (an empty karma feed hides itself).
|
|
740
|
-
// Re-run renderNav so the group — and its rail entry — appear.
|
|
741
|
-
if (lastMeState?.builder) renderNav(lastMeState.builder, lastMeState.onboarding || null);
|
|
742
|
-
renderAcclaimRows();
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function renderAcclaimRows() {
|
|
746
|
-
const tbody = $('#acclaim-feed tbody');
|
|
747
|
-
if (!tbody) return;
|
|
748
|
-
const slice = window.OTBKit.paginate(acclaimState.tasks, acclaimPager);
|
|
749
|
-
tbody.innerHTML = slice.map((t) => {
|
|
750
|
-
// task 1004432: an approved page is its artist's; the applying session rides along as a helper.
|
|
751
|
-
const helperName = t.helper_display_name || (t.helper_login ? `@${t.helper_login}` : '');
|
|
752
|
-
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>` : '');
|
|
753
|
-
// task 1004833: the votes are THE shared list control (vote-control.js),
|
|
754
|
-
// pre-painted from the feed's own net_votes/my_vote so it costs no extra
|
|
755
|
-
// read. A viewer below the vote floor sees the net alone.
|
|
756
|
-
const net = Number(t.net_votes) || 0;
|
|
757
|
-
const votes = acclaimCanVote && window.OTBVote
|
|
758
|
-
? window.OTBVote.taskHtml(t)
|
|
759
|
-
: `<span class="vote__net" title="Metic rank and above may vote">${net > 0 ? `+${net}` : `${net}`}</span>`;
|
|
760
|
-
const title = escapeHtml(t.value_summary || t.title || `task #${t.id}`);
|
|
761
|
-
return `
|
|
762
|
-
<tr>
|
|
763
|
-
<td>${title}${acclaimEvidenceHtml(t)}</td>
|
|
764
|
-
<td>${by}</td>
|
|
765
|
-
<td>${votes}</td>
|
|
766
|
-
</tr>
|
|
767
|
-
`;
|
|
768
|
-
}).join('');
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
async function loadAcclaimFeed() {
|
|
772
|
-
try {
|
|
773
|
-
const data = await getJSON(`${API}/tasks/peer-votes/feed?limit=25`);
|
|
774
|
-
renderAcclaimFeed(data);
|
|
775
|
-
} catch (err) {
|
|
776
|
-
// Non-fatal: a feed failure shouldn't take down the hall.
|
|
777
|
-
console.warn('[hall] acclaim feed failed:', err.message);
|
|
778
|
-
const scroll = $('#acclaim-scroll');
|
|
779
|
-
if (scroll) scroll.hidden = true;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// task 1004833: the shared vote control casts the vote and repaints its own
|
|
784
|
-
// row; it then raises 'otb-vote' with the server's answer. The feed is paged,
|
|
785
|
-
// so the vote must land in the HELD rows too (task 1002446), or flipping to
|
|
786
|
-
// page 2 and back re-renders the stale tally over the one just cast.
|
|
787
|
-
function onAcclaimVote(ev) {
|
|
788
|
-
const d = ev && ev.detail;
|
|
789
|
-
if (!d || d.kind !== 'task') return;
|
|
790
|
-
const taskIdNum = Number(d.id);
|
|
791
|
-
acclaimState.tasks = acclaimState.tasks.map((t) => (Number(t.id) === taskIdNum
|
|
792
|
-
? { ...t, net_votes: d.net_votes, my_vote: d.my_vote }
|
|
793
|
-
: t));
|
|
794
|
-
}
|
|
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.)
|
|
795
272
|
|
|
796
273
|
// ---- task browser (removed #750) -----------------------------------------
|
|
797
274
|
// The claimable-works list now lives solely on the /work Work Board (#748);
|
|
@@ -888,79 +365,6 @@
|
|
|
888
365
|
// builder isn't on the roll). Matched by github_login (rows carry no id).
|
|
889
366
|
|
|
890
367
|
|
|
891
|
-
// ---- onboarding live-tick poll (task 1444) -------------------------------
|
|
892
|
-
//
|
|
893
|
-
// The onboarding checklist is rendered once from the page-load /me snapshot.
|
|
894
|
-
// But two of its steps clear from LIVE server signals that flip WITHOUT any
|
|
895
|
-
// local action or page reload while the hall sits open:
|
|
896
|
-
// * cli_setup — stamped when the Bongos CLI is first authenticated;
|
|
897
|
-
// * join_discord — flips on the OAuth link (usually a full redirect, but can
|
|
898
|
-
// also land from another tab).
|
|
899
|
-
// Without a poll, a newcomer who signs the CLI in AFTER opening the hall sees
|
|
900
|
-
// the step stay unchecked until they manually reload — exactly the bug scootermack
|
|
901
|
-
// hit. "The city watches" only worked on the next full reload. So while any
|
|
902
|
-
// live-signal step is still uncleared, re-fetch /me on an interval and re-render
|
|
903
|
-
// the checklist; stop once they all clear (or the builder graduates).
|
|
904
|
-
let onboardingPollTimer = null;
|
|
905
|
-
const ONBOARDING_POLL_MS = 15000;
|
|
906
|
-
|
|
907
|
-
// True while at least one step that clears from a live signal (cli/discord)
|
|
908
|
-
// is still uncleared and the builder hasn't graduated. The pick_disciplines and
|
|
909
|
-
// ship_three steps flip via local action (PATCH re-renders) or shipping, so they
|
|
910
|
-
// don't need a poll — scoping to the live-signal steps bounds polling to the
|
|
911
|
-
// setup window rather than running forever for any non-graduated builder.
|
|
912
|
-
function onboardingNeedsLivePoll(onboarding) {
|
|
913
|
-
if (!onboarding || onboarding.graduated) return false;
|
|
914
|
-
const stages = Array.isArray(onboarding.stages) ? onboarding.stages : [];
|
|
915
|
-
return stages.some((s) =>
|
|
916
|
-
(s.source === 'cli' || s.source === 'discord') && !s.cleared);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function stopOnboardingPoll() {
|
|
920
|
-
if (onboardingPollTimer) { clearTimeout(onboardingPollTimer); onboardingPollTimer = null; }
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function scheduleOnboardingPoll(onboarding) {
|
|
924
|
-
stopOnboardingPoll();
|
|
925
|
-
if (!onboardingNeedsLivePoll(onboarding)) return;
|
|
926
|
-
onboardingPollTimer = setTimeout(refreshOnboardingFromServer, ONBOARDING_POLL_MS);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
async function refreshOnboardingFromServer() {
|
|
930
|
-
onboardingPollTimer = null;
|
|
931
|
-
// Don't poll a backgrounded tab — wait for it to come back (the
|
|
932
|
-
// visibilitychange listener re-arms from the cached state).
|
|
933
|
-
if (document.hidden) { scheduleOnboardingPoll(lastMeState && lastMeState.onboarding); return; }
|
|
934
|
-
let me;
|
|
935
|
-
try {
|
|
936
|
-
me = await getJSON(`${API}/me`, { softAuth: true });
|
|
937
|
-
} catch (err) {
|
|
938
|
-
// Session expired → stop (a redirect/landing will take over on next action).
|
|
939
|
-
if (err && err.code === AUTH_NEEDED) { stopOnboardingPoll(); return; }
|
|
940
|
-
// Transient (network blip / 5xx) → try again next interval so a single
|
|
941
|
-
// failure doesn't permanently kill the auto-tick.
|
|
942
|
-
scheduleOnboardingPoll(lastMeState && lastMeState.onboarding);
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
if (!me || !me.builder) return;
|
|
946
|
-
const wasGraduated = !!(lastMeState && lastMeState.onboarding && lastMeState.onboarding.graduated);
|
|
947
|
-
lastMeState = me;
|
|
948
|
-
lastKarmaState = me.karma || lastKarmaState;
|
|
949
|
-
lastCostState = me.cost || lastCostState;
|
|
950
|
-
lastNeedsState = me.needs || lastNeedsState;
|
|
951
|
-
// ADR 0120 part 5: owed-rebase list is authoritative on every /me (an empty
|
|
952
|
-
// array means "nothing owed now"), so take it verbatim when present.
|
|
953
|
-
if (Array.isArray(me.needs_rebase_tasks)) lastRebaseState = me.needs_rebase_tasks;
|
|
954
|
-
renderOnboarding(me.onboarding || null, me.discord);
|
|
955
|
-
// If they graduated while watching, hide the welcome panel + rebuild the nav
|
|
956
|
-
// (drops the Welcome/Path chips) — the same transition loadAll would render.
|
|
957
|
-
if (me.onboarding && me.onboarding.graduated && !wasGraduated) {
|
|
958
|
-
renderWelcome(me.builder, me.onboarding);
|
|
959
|
-
renderNav(me.builder, me.onboarding || null);
|
|
960
|
-
}
|
|
961
|
-
scheduleOnboardingPoll(me.onboarding || null);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
368
|
// ---- bootstrap -----------------------------------------------------------
|
|
965
369
|
|
|
966
370
|
// `prefetchedMe` is boot()'s already-resolved /me — the SAME payload, handed in
|
|
@@ -986,52 +390,22 @@
|
|
|
986
390
|
lastCostState = me.cost || null;
|
|
987
391
|
lastNeedsState = me.needs || null; // task 1013 / ADR 0073: action-needed banner
|
|
988
392
|
lastRebaseState = Array.isArray(me.needs_rebase_tasks) ? me.needs_rebase_tasks : null; // ADR 0120 part 5: owed-rebase banner
|
|
989
|
-
// #408: stash the whole /me payload so reopenWelcomeFromNav can re-render
|
|
990
|
-
// the welcome panel without a fresh round-trip.
|
|
991
393
|
lastMeState = me;
|
|
992
394
|
// #748: decide the newcomer gate up front so the async karma/acclaim feeds
|
|
993
395
|
// (which un-hide their own scroll on data arrival) honour it too.
|
|
994
396
|
newcomerGated = normalizeRank(me.builder?.rank) === 'xenos';
|
|
995
|
-
//
|
|
996
|
-
// hides itself once dismissed or once the builder graduates. Computed
|
|
997
|
-
// before profile so the dismiss button is wired by the time the user
|
|
998
|
-
// sees the page.
|
|
999
|
-
// Cache self id on window so the dismiss-click handler can find it
|
|
1000
|
-
// without re-fetching /me. Set BEFORE renderWelcome so the wired
|
|
1001
|
-
// dismiss listener (attached once at bootstrap) can use it immediately.
|
|
397
|
+
// Cache self id on window for handlers wired once at bootstrap.
|
|
1002
398
|
window.__buildersSelfId = me.builder?.id ?? null;
|
|
1003
|
-
renderWelcome(me.builder, me.onboarding || null);
|
|
1004
399
|
renderProfile(me.builder);
|
|
1005
400
|
renderGreeting(); // task 1003464: "Good morning, <name>" — the h1 — as soon as /me answers
|
|
1006
401
|
// #731: the CLI-token re-issue card (V3.R91 #401) and the Discord connect/
|
|
1007
402
|
// link card (F7, ADR 0033) moved to /builders/settings — the hall's Standing
|
|
1008
403
|
// card is identity-only now.
|
|
1009
|
-
// V3.R75 (#294): auto-completing onboarding checklist. The /me payload
|
|
1010
|
-
// carries the builder's own state directly so this is a render-only call.
|
|
1011
|
-
renderOnboarding(me.onboarding || null, me.discord);
|
|
1012
|
-
// task 1444: keep the checklist live — re-fetch /me on an interval while a
|
|
1013
|
-
// cli/discord step is still uncleared, so it ticks itself without a manual
|
|
1014
|
-
// reload the moment the CLI or Discord links.
|
|
1015
|
-
scheduleOnboardingPoll(me.onboarding || null);
|
|
1016
|
-
renderRecentGrades(me.grades?.recent || []);
|
|
1017
|
-
renderRollingNote(me.grades?.rolling || null);
|
|
1018
|
-
// V3.R72 (#291, C5): the dev curve. Loaded fire-and-forget — a slow or
|
|
1019
|
-
// empty analytics feed must not hold up the rest of the hall, and the
|
|
1020
|
-
// section un-hides itself (and re-renders the nav) only when there's data.
|
|
1021
|
-
loadAnalytics(me.builder);
|
|
1022
|
-
// V3.R26 (#246): karma feed — the recent karma_log entries powering the
|
|
1023
|
-
// "why did my karma move" answer. The /me payload bundles this so the
|
|
1024
|
-
// page doesn't need a follow-up call.
|
|
1025
|
-
renderKarmaFeed(me.karma?.recent || []);
|
|
1026
|
-
// V3.R25 (#245): peer acclaim feed — a separate endpoint (not bundled in
|
|
1027
|
-
// /me because it spans all builders' work), loaded fire-and-forget so a
|
|
1028
|
-
// slow/failed feed can't hold up the rest of the hall.
|
|
1029
|
-
loadAcclaimFeed();
|
|
1030
404
|
// #408: build the sticky nav now that every section has settled its
|
|
1031
|
-
// hidden state (
|
|
405
|
+
// hidden state (karma → security/roster).
|
|
1032
406
|
// The renderNav call also wires the IntersectionObserver for active-
|
|
1033
|
-
// section highlighting. Idempotent — safe to call again
|
|
1034
|
-
// #748: collapse the hall to the newcomer view (
|
|
407
|
+
// section highlighting. Idempotent — safe to call again.
|
|
408
|
+
// #748: collapse the hall to the newcomer view (Standing) for
|
|
1035
409
|
// a Xenos BEFORE building the nav, so the chip strip matches what's shown.
|
|
1036
410
|
applyNewcomerGate();
|
|
1037
411
|
renderNav(me.builder, me.onboarding || null);
|
|
@@ -1046,41 +420,15 @@
|
|
|
1046
420
|
// down the page — log and show an inline placeholder.
|
|
1047
421
|
// #726: the per-skill model picker (Voices) moved to /builders/settings,
|
|
1048
422
|
// so the hall no longer fetches /me/skill-prefs here.
|
|
1049
|
-
const [
|
|
1050
|
-
|
|
1051
|
-
//
|
|
1052
|
-
// 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).
|
|
1053
426
|
getJSON(`${API}/me/recent-ships?limit=${HOME_SHIPS_LIMIT}`),
|
|
1054
|
-
getJSON(`${API}/versions`),
|
|
1055
427
|
getJSON(`${API}/public/leaderboard`), // task 1004437: the list with each builder's roles
|
|
1056
428
|
]);
|
|
1057
429
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
// for V2, so the slice is defensive.) Unlocked + locked-with-progress come
|
|
1061
|
-
// straight from the enriched /me payload.
|
|
1062
|
-
renderAchievements(
|
|
1063
|
-
(achAll.value.achievements || []).slice(0, 10),
|
|
1064
|
-
me.unlocked || [],
|
|
1065
|
-
me.locked_with_progress || [],
|
|
1066
|
-
);
|
|
1067
|
-
} else {
|
|
1068
|
-
$('#ach-grid').innerHTML = `<div class="profile__placeholder">Achievements could not be loaded.</div>`;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
if (ships.status === 'fulfilled') {
|
|
1072
|
-
setRecentShips(ships.value);
|
|
1073
|
-
} else {
|
|
1074
|
-
$('#recent-ships tbody').innerHTML = emptyRow(4, 'Your work history could not be loaded.');
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
if (versionsResp.status === 'fulfilled') {
|
|
1078
|
-
const versions = versionsResp.value.versions || [];
|
|
1079
|
-
// Populate the Works Wrought version filter — limited to versions this
|
|
1080
|
-
// builder has actually shipped to, so the dropdown can't offer empty
|
|
1081
|
-
// options. (The old claimable-works version filter moved to /work, #750.)
|
|
1082
|
-
populateShipVersionFilter(versions);
|
|
1083
|
-
}
|
|
430
|
+
// Only the Standing chip's shipped count reads this; a failure leaves its dash.
|
|
431
|
+
if (ships.status === 'fulfilled') setRecentShips(ships.value);
|
|
1084
432
|
|
|
1085
433
|
if (lb.status === 'fulfilled') {
|
|
1086
434
|
lbState.rows = lb.value.leaderboard || [];
|
|
@@ -1097,78 +445,6 @@
|
|
|
1097
445
|
}
|
|
1098
446
|
|
|
1099
447
|
function wireEvents() {
|
|
1100
|
-
// #profile-body is re-rendered after load, so delegate the save click.
|
|
1101
|
-
$('#profile-body').addEventListener('click', (e) => {
|
|
1102
|
-
if (e.target.closest('#disc-save')) saveDisciplines();
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
// V3.R72 (#291): the dev-curve scope toggle. Delegated because
|
|
1106
|
-
// renderCurveBody replaces the toggle's innerHTML — and from the whole
|
|
1107
|
-
// Progress section, NOT #curve-body: the controls render into
|
|
1108
|
-
// #curve-scope-note in the section's lede, a sibling of #curve-body, so a
|
|
1109
|
-
// listener there never heard them (task 1004079).
|
|
1110
|
-
$('#analytics-scroll')?.addEventListener('click', (e) => {
|
|
1111
|
-
const btn = e.target.closest('.curve__scope-btn[data-scope]');
|
|
1112
|
-
if (!btn) return;
|
|
1113
|
-
const scope = btn.dataset.scope === 'eco' ? 'eco' : 'self';
|
|
1114
|
-
if (scope === analyticsState.scope) return;
|
|
1115
|
-
analyticsState.scope = scope;
|
|
1116
|
-
renderCurveBody();
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
// The kind filter (task 1004005). Delegated like the scope toggle, and for
|
|
1120
|
-
// the same reason: renderCurveBody replaces the control's innerHTML, so a
|
|
1121
|
-
// listener bound to the element itself would not survive the first render.
|
|
1122
|
-
// 'change', not 'input' — a keyboard user arrowing through a native select
|
|
1123
|
-
// fires input per option, and each one would be a pair of round trips.
|
|
1124
|
-
$('#analytics-scroll')?.addEventListener('change', (e) => {
|
|
1125
|
-
const sel = e.target.closest('#curve-kind');
|
|
1126
|
-
if (!sel) return;
|
|
1127
|
-
const kind = sel.value ? sel.value : null;
|
|
1128
|
-
if (kind === analyticsState.kind) return;
|
|
1129
|
-
reloadCurveForKind(kind);
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
// task 1723: the Home "show the setup guide again" link re-opens a dismissed
|
|
1133
|
-
// welcome banner (the old welcome chip's job) — clears the dismissal flag,
|
|
1134
|
-
// re-renders the panel, refreshes the nav, then scrolls to it.
|
|
1135
|
-
document.getElementById('welcome-reopen')?.addEventListener('click', (e) => {
|
|
1136
|
-
reopenWelcomeFromNav(e);
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
// V3.R75 (#294): the onboarding list re-renders on each /me load, so the
|
|
1140
|
-
// inline craft picker is wired by delegation to the list container. (The
|
|
1141
|
-
// primer-read ack button that also lived here was removed in #766.)
|
|
1142
|
-
$('#onboarding-list')?.addEventListener('click', (e) => {
|
|
1143
|
-
const disc = e.target.closest('.onboarding-item__disc[data-disc]');
|
|
1144
|
-
if (disc) { pickDiscipline(disc.dataset.disc); return; }
|
|
1145
|
-
// #777: the only other control is the Connect-Discord anchor, which is a
|
|
1146
|
-
// real link (full-page nav into the OAuth flow) — nothing to handle here.
|
|
1147
|
-
// Both new steps (join_discord, cli_setup) auto-detect server-side.
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
// Welcome panel minimize toggle (replaces full dismiss).
|
|
1151
|
-
$('#welcome-minimize')?.addEventListener('click', () => {
|
|
1152
|
-
const scroll = $('#welcome-scroll');
|
|
1153
|
-
const btn = $('#welcome-minimize');
|
|
1154
|
-
if (!scroll) return;
|
|
1155
|
-
const minimized = scroll.classList.toggle('scroll--welcome--minimized');
|
|
1156
|
-
if (btn) btn.textContent = minimized ? '+' : '−';
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
// task 1004833: the shared vote control reports each landed vote to the feed.
|
|
1160
|
-
$('#acclaim-feed')?.addEventListener('otb-vote', onAcclaimVote);
|
|
1161
|
-
|
|
1162
|
-
$('#f-ships-window')?.addEventListener('change', onShipsFilterChange);
|
|
1163
|
-
$('#f-ships-version')?.addEventListener('change', onShipsFilterChange);
|
|
1164
|
-
$('#f-ships-sort')?.addEventListener('change', renderRecentShips);
|
|
1165
|
-
// task 2174: the kit wires prev/next/per-page for both tables against the
|
|
1166
|
-
// #ships-*/#lb-* ids its own pagerBarHtml() stamped in hall-widgets.js.
|
|
1167
|
-
window.OTBKit.wirePager(shipsPager, renderRecentShips);
|
|
1168
|
-
|
|
1169
|
-
window.OTBKit.wirePager(acclaimPager, renderAcclaimRows);
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
448
|
// Sign-out is wired centrally in shell.js (doLogout + a delegated click
|
|
1173
449
|
// listener on the sign-out anchors), so it runs the real POST -> cookie-clear
|
|
1174
450
|
// -> bounce-home on EVERY hall page, not only this index. It used to live
|
|
@@ -1273,9 +549,8 @@
|
|
|
1273
549
|
// reads what this page holds NOW rather than what it held at boot. Everything
|
|
1274
550
|
// else is stable for the life of the page. This call sits AFTER every
|
|
1275
551
|
// declaration it passes — moving it earlier is a temporal-dead-zone crash.
|
|
1276
|
-
const {
|
|
1277
|
-
API, RANK_ORDER, api, brandWorld, curLabel,
|
|
1278
|
-
get instanceRepo() { return instanceRepo; },
|
|
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,
|
|
1279
554
|
get lastCostState() { return lastCostState; },
|
|
1280
555
|
get lastKarmaState() { return lastKarmaState; },
|
|
1281
556
|
get lastMeState() { return lastMeState; },
|
|
@@ -1318,9 +593,6 @@
|
|
|
1318
593
|
// which only exist once the registry has mounted them.
|
|
1319
594
|
mountHallWidgets();
|
|
1320
595
|
wireEvents();
|
|
1321
|
-
// Fetch the instance's repo coords (public) so the welcome "get the code"
|
|
1322
|
-
// clone block shows this instance's own repository (ADR 0062 §3, task 1705).
|
|
1323
|
-
loadInstanceRepo();
|
|
1324
596
|
// #731: the Discord connect/link card moved to /builders/settings, and the
|
|
1325
597
|
// OAuth callback now lands there — so the ?discord=joined|linked toast is
|
|
1326
598
|
// handled by settings.js, not here.
|