@bongos/core 1.19.482 → 1.19.483
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 +17 -17
- package/docs/copy-inventory.md +100 -94
- package/docs/copy-registry.json +183 -129
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/builders.js +20 -2
- package/modules/hall-ui/public/goal-inbox.js +7 -0
- package/modules/hall-ui/public/hall-render.js +108 -17
- package/modules/hall-ui/public/index.html +11 -1
- package/modules/hall-ui/public/style.css +70 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_board_world.mjs +199 -0
|
@@ -1401,5 +1401,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1401
1401
|
landed since 1.19.480 with no explicit bump. run 33670051626. (task 1002620)
|
|
1402
1402
|
1.19.482 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1403
1403
|
landed since 1.19.481 with no explicit bump. run 33670786915. (task 1002620)
|
|
1404
|
+
1.19.483 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1405
|
+
landed since 1.19.482 with no explicit bump. run 33674657478. (task 1002620)
|
|
1404
1406
|
---------------------------------------------------------------------------
|
|
1405
1407
|
```
|
|
@@ -173,6 +173,11 @@
|
|
|
173
173
|
// (same pattern as lastNeedsState). These are un-landed confirmed tasks that
|
|
174
174
|
// GATE new claims and whose reward is pending until they land on ship.
|
|
175
175
|
let lastRebaseState = null;
|
|
176
|
+
// task 1003464: the two halves of the greeting's status sentence — the
|
|
177
|
+
// public version progress and how many tasks the claimable feed returned.
|
|
178
|
+
// Null until each lands; the sentence says only what it knows.
|
|
179
|
+
let lastProgressState = null;
|
|
180
|
+
let lastClaimableCount = null;
|
|
176
181
|
|
|
177
182
|
// Cached /me payload from the most recent loadAll() call. Used by the
|
|
178
183
|
// welcome-chip reopen path (#408) so we can re-render the welcome panel
|
|
@@ -891,6 +896,10 @@
|
|
|
891
896
|
if (!wrap || !list) return;
|
|
892
897
|
try {
|
|
893
898
|
const resp = await getJSON(`${API}/tasks/claimable`, { softAuth: true });
|
|
899
|
+
// task 1003464: the same feed answers the greeting's "N tasks are
|
|
900
|
+
// claimable right now" — rank-scoped, so it is THIS builder's number.
|
|
901
|
+
lastClaimableCount = resp && Array.isArray(resp.claimable) ? resp.claimable.length : null;
|
|
902
|
+
renderGreeting();
|
|
894
903
|
const rows = (resp && Array.isArray(resp.claimable) ? resp.claimable : []).slice(0, 2);
|
|
895
904
|
if (!rows.length) { wrap.hidden = true; if (subhead) subhead.hidden = true; return; }
|
|
896
905
|
const cur = (window.__BRANDING__ && window.__BRANDING__.currency && window.__BRANDING__.currency.label) || 'credits';
|
|
@@ -1039,6 +1048,7 @@
|
|
|
1039
1048
|
window.__buildersSelfId = me.builder?.id ?? null;
|
|
1040
1049
|
renderWelcome(me.builder, me.onboarding || null);
|
|
1041
1050
|
renderProfile(me.builder);
|
|
1051
|
+
renderGreeting(); // task 1003464: "Good morning, <name>" — the h1 — as soon as /me answers
|
|
1042
1052
|
// #731: the CLI-token re-issue card (V3.R91 #401) and the Discord connect/
|
|
1043
1053
|
// link card (F7, ADR 0033) moved to /builders/settings — the hall's Standing
|
|
1044
1054
|
// card is identity-only now.
|
|
@@ -1087,13 +1097,19 @@
|
|
|
1087
1097
|
// down the page — log and show an inline placeholder.
|
|
1088
1098
|
// #726: the per-skill model picker (Voices) moved to /builders/settings,
|
|
1089
1099
|
// so the hall no longer fetches /me/skill-prefs here.
|
|
1090
|
-
const [achAll, ships, versionsResp, lb] = await Promise.allSettled([
|
|
1100
|
+
const [achAll, ships, versionsResp, lb, progress] = await Promise.allSettled([
|
|
1091
1101
|
getJSON(`${API}/achievements`),
|
|
1092
1102
|
getJSON(`${API}/me/recent-ships?limit=all`),
|
|
1093
1103
|
getJSON(`${API}/versions`),
|
|
1094
1104
|
getJSON(`${API}/leaderboard`),
|
|
1105
|
+
getJSON(`${API}/public/progress`), // task 1003464: "<version> is N percent shipped"
|
|
1095
1106
|
]);
|
|
1096
1107
|
|
|
1108
|
+
if (progress.status === 'fulfilled') {
|
|
1109
|
+
lastProgressState = progress.value;
|
|
1110
|
+
renderGreeting();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1097
1113
|
if (achAll.status === 'fulfilled') {
|
|
1098
1114
|
// Show top 10 by sort_order — already ordered server-side. (Set is locked at 8
|
|
1099
1115
|
// for V2, so the slice is defensive.) Unlocked + locked-with-progress come
|
|
@@ -1314,13 +1330,15 @@
|
|
|
1314
1330
|
// reads what this page holds NOW rather than what it held at boot. Everything
|
|
1315
1331
|
// else is stable for the life of the page. This call sits AFTER every
|
|
1316
1332
|
// declaration it passes — moving it earlier is a temporal-dead-zone crash.
|
|
1317
|
-
const { SETUP_CONFIG_KEY, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey } = window.OTBHallRender({
|
|
1333
|
+
const { SETUP_CONFIG_KEY, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey } = window.OTBHallRender({
|
|
1318
1334
|
API, RANK_ORDER, api, brandWorld, curLabel, emptyRow, fmtDate, mins, normalizeRank, pct, postJSON, rankLabel, rankWeight, signClasses, toast, usd,
|
|
1319
1335
|
get instanceRepo() { return instanceRepo; },
|
|
1336
|
+
get lastClaimableCount() { return lastClaimableCount; },
|
|
1320
1337
|
get lastCostState() { return lastCostState; },
|
|
1321
1338
|
get lastKarmaState() { return lastKarmaState; },
|
|
1322
1339
|
get lastMeState() { return lastMeState; },
|
|
1323
1340
|
get lastNeedsState() { return lastNeedsState; },
|
|
1341
|
+
get lastProgressState() { return lastProgressState; },
|
|
1324
1342
|
get lastRebaseState() { return lastRebaseState; },
|
|
1325
1343
|
get newcomerGated() { return newcomerGated; },
|
|
1326
1344
|
});
|
|
@@ -272,6 +272,13 @@
|
|
|
272
272
|
host.hidden = true; // stays hidden until a slice finds something actionable
|
|
273
273
|
host.innerHTML =
|
|
274
274
|
'<h2 id="attention-h" class="scroll__h">Needs your attention<span class="count-pill" id="attention-count" hidden></span></h2>' +
|
|
275
|
+
// task 1003464: the system's own notices — rebase owed, an input the
|
|
276
|
+
// system needs, the monthly budget — used to stack as banners INSIDE the
|
|
277
|
+
// Standing card. They are lines here now (rendered by hall-render.js
|
|
278
|
+
// renderSystemNotices from the /me state it already holds), first because
|
|
279
|
+
// a paused queue outranks an invitation. Rows carry .ginbox__item so
|
|
280
|
+
// refresh() counts them.
|
|
281
|
+
'<div id="attention-system"></div>' +
|
|
275
282
|
'<div id="attention-goals"></div>' +
|
|
276
283
|
// task 1002802 (P06): "Complete your profile" — a DISMISSIBLE nudge when
|
|
277
284
|
// bio or preferred_disciplines are empty. Rendered by builders.js (which
|
|
@@ -508,7 +508,7 @@
|
|
|
508
508
|
</div>
|
|
509
509
|
<div class="profile__disciplines">
|
|
510
510
|
<span class="profile__disc-label">Preferred work:</span>
|
|
511
|
-
<span class="profile__disc-value">${prefs.length ? escapeHtml(prefs.join(', ')) :
|
|
511
|
+
<span class="profile__disc-value">${prefs.length ? escapeHtml(prefs.join(', ')) : PREFS_EMPTY}</span>
|
|
512
512
|
<a class="profile__disc-edit" href="/settings">Change in Settings →</a>
|
|
513
513
|
</div>
|
|
514
514
|
</div>
|
|
@@ -542,30 +542,121 @@
|
|
|
542
542
|
const statsSection = document.getElementById('home-stats');
|
|
543
543
|
if (statsSection && !ctx.newcomerGated) statsSection.hidden = false;
|
|
544
544
|
|
|
545
|
+
// task 1003464: the Home card as the mock draws it — an identity LINE and a
|
|
546
|
+
// three-row fact ledger. The mock repeats Credits and the place even though
|
|
547
|
+
// the stat band above carries both; the card is built as approved, all three
|
|
548
|
+
// rows, because that content is also what makes it end level with Goals.
|
|
549
|
+
// The notices that used to stack in here are inbox lines now.
|
|
545
550
|
const homeStanding = document.getElementById('home-standing-body');
|
|
546
551
|
if (homeStanding) {
|
|
552
|
+
const place = rollPlace ? `${ordinal(rollPlace.place)} of ${fmtNum(rollPlace.total)}` : '—';
|
|
547
553
|
homeStanding.innerHTML = `
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
<
|
|
555
|
-
<p class="profile__name">${escapeHtml(builder.display_name || builder.github_login || 'Builder')}</p>
|
|
556
|
-
${rankBadge}
|
|
557
|
-
</div>
|
|
558
|
-
<p class="profile__handle">@${escapeHtml(builder.github_login || 'unknown')}</p>
|
|
559
|
-
<div class="profile__stats">
|
|
560
|
-
<span>Member since ${fmtDate(builder.created_at)}</span>
|
|
561
|
-
<span>${prefs.length ? escapeHtml(prefs.join(', ')) : 'no preferred work yet'}</span>
|
|
562
|
-
</div>
|
|
554
|
+
<div class="home-who">
|
|
555
|
+
${builder.avatar_url
|
|
556
|
+
? `<img class="home-who__avatar" src="${escapeHtml(builder.avatar_url)}" alt="">`
|
|
557
|
+
: '<span class="home-who__avatar" aria-hidden="true"></span>'}
|
|
558
|
+
<div class="home-who__meta">
|
|
559
|
+
<p class="home-who__name">${escapeHtml(builder.display_name || builder.github_login || 'Builder')}</p>
|
|
560
|
+
<p class="home-who__handle">@${escapeHtml(builder.github_login || 'unknown')}</p>
|
|
563
561
|
</div>
|
|
562
|
+
<span class="home-who__rank">${builder.rank ? renderRankBadge(normalizeRank(builder.rank), { tip: false }) : ''}</span>
|
|
564
563
|
</div>
|
|
564
|
+
<dl class="home-facts">
|
|
565
|
+
<div><dt>${escapeHtml(curLabel(true))}</dt><dd>${fmtNum(builder.total_credits)}</dd></div>
|
|
566
|
+
<div><dt>On the board</dt><dd>${place}</dd></div>
|
|
567
|
+
<div><dt>Preferred work</dt><dd>${prefs.length ? escapeHtml(prefs.join(', ')) : PREFS_EMPTY}</dd></div>
|
|
568
|
+
</dl>
|
|
565
569
|
`;
|
|
566
570
|
}
|
|
567
571
|
const homeStandingSection = document.getElementById('home-standing');
|
|
568
572
|
if (homeStandingSection && !ctx.newcomerGated) homeStandingSection.hidden = false;
|
|
573
|
+
renderSystemNotices({ rebaseTasks, need, cost });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// The ONE string for "no preferred work chosen" — the mock's copy — on both
|
|
577
|
+
// Standing surfaces (the Home card and the full view), so they cannot drift.
|
|
578
|
+
const PREFS_EMPTY = 'Not set yet';
|
|
579
|
+
|
|
580
|
+
const ordinal = (n) => {
|
|
581
|
+
const v = Number(n) || 0;
|
|
582
|
+
const s = (v % 100 >= 11 && v % 100 <= 13) ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' }[v % 10] || 'th');
|
|
583
|
+
return `${v}${s}`;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// task 1003464: the three system notices as inbox LINES on Home (the
|
|
587
|
+
// placement rule in modules/hall-ui/CLAUDE.md: an action awaiting the builder
|
|
588
|
+
// is a line item in Needs-attention, never a card). Only what awaits the
|
|
589
|
+
// builder lands here — a "we cover it" note is information, and stays on the
|
|
590
|
+
// full Standing view with the other two, where the banners still render.
|
|
591
|
+
function renderSystemNotices({ rebaseTasks, need, cost }) {
|
|
592
|
+
const box = document.getElementById('attention-system');
|
|
593
|
+
if (!box) return;
|
|
594
|
+
const rows = [];
|
|
595
|
+
if (rebaseTasks.length) {
|
|
596
|
+
const refs = rebaseTasks.map((t) => {
|
|
597
|
+
const id = String(t.id);
|
|
598
|
+
const href = window.OTBTaskDetail && typeof window.OTBTaskDetail.href === 'function' ? window.OTBTaskDetail.href(id) : `/task/${encodeURIComponent(id)}`;
|
|
599
|
+
return `<a href="${escapeHtml(href)}">task ${escapeHtml(id)}</a>`;
|
|
600
|
+
}).join(', ');
|
|
601
|
+
const one = rebaseTasks.length === 1;
|
|
602
|
+
rows.push(`<li class="ginbox__item ginbox__item--danger"><div class="ginbox__body">` +
|
|
603
|
+
`<span class="ginbox__kind">Rebase owed</span>` +
|
|
604
|
+
`<span class="ginbox__line">New claims are paused: ${rebaseTasks.length} confirmed ${one ? 'task' : 'tasks'} can't land yet, so ${one ? 'its' : 'their'} reward is pending. Rebase and re-ship, or release: ${refs}.</span>` +
|
|
605
|
+
`</div></li>`);
|
|
606
|
+
}
|
|
607
|
+
if (need && need.state === 'action_needed') {
|
|
608
|
+
const act = need.action && need.action.href
|
|
609
|
+
? `<div class="ginbox__actions"><a class="btn-ghost" href="${escapeHtml(need.action.href)}">${escapeHtml(need.action.label || 'Set it up')}</a></div>`
|
|
610
|
+
: '';
|
|
611
|
+
rows.push(`<li class="ginbox__item ginbox__item--warn"><div class="ginbox__body">` +
|
|
612
|
+
`<span class="ginbox__kind">${escapeHtml(need.title)}</span>` +
|
|
613
|
+
`<span class="ginbox__line">${escapeHtml(need.message)}</span>` +
|
|
614
|
+
`</div>${act}</li>`);
|
|
615
|
+
}
|
|
616
|
+
if (cost && (cost.level === 'warn' || cost.level === 'over')) {
|
|
617
|
+
const over = cost.level === 'over';
|
|
618
|
+
rows.push(`<li class="ginbox__item ginbox__item--${over ? 'danger' : 'warn'}"><div class="ginbox__body">` +
|
|
619
|
+
`<span class="ginbox__kind">${over ? 'Monthly budget exceeded' : 'Approaching monthly budget'}</span>` +
|
|
620
|
+
`<span class="ginbox__line">$${escapeHtml(String(cost.spend_mtd_usd))} of $${escapeHtml(String(cost.budget_mtd_usd))} this month (${escapeHtml(String(cost.pct_used))}%). ${over ? 'Shipping and art generation are paused until an Archon raises your budget.' : 'At 100% these actions pause until an Archon raises your budget.'}</span>` +
|
|
621
|
+
`</div></li>`);
|
|
622
|
+
}
|
|
623
|
+
box.innerHTML = rows.length ? `<h3 class="ginbox__sec-h">From the system</h3><ul class="ginbox">${rows.join('')}</ul>` : '';
|
|
624
|
+
if (window.OTBAttention && typeof window.OTBAttention.refresh === 'function') window.OTBAttention.refresh();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// task 1003464: the greeting IS Home's one h1 (the mock opens with it), and
|
|
628
|
+
// the sub-line is the live status sentence — how far the version is and
|
|
629
|
+
// whether there is anything to do — the one line a returning builder needs.
|
|
630
|
+
// Each half renders as its data lands; the line stays hidden until one has.
|
|
631
|
+
const SMALL_NUMBERS = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve'];
|
|
632
|
+
const greetingWord = (hour) => (hour < 12 ? 'Good morning' : (hour < 18 ? 'Good afternoon' : 'Good evening'));
|
|
633
|
+
// The version the sentence is about: the one being built with the most work in it.
|
|
634
|
+
function currentVersion(progress) {
|
|
635
|
+
const rows = progress && Array.isArray(progress.progress) ? progress.progress : [];
|
|
636
|
+
return rows.filter((r) => r && r.status === 'building')
|
|
637
|
+
.sort((a, b) => (Number(b.total_count) || 0) - (Number(a.total_count) || 0))[0] || null;
|
|
638
|
+
}
|
|
639
|
+
function renderGreeting() {
|
|
640
|
+
const h1 = document.getElementById('hall-h1');
|
|
641
|
+
const sub = document.getElementById('hall-sub');
|
|
642
|
+
const builder = ctx.lastMeState && ctx.lastMeState.builder;
|
|
643
|
+
if (h1 && builder) {
|
|
644
|
+
h1.textContent = `${greetingWord(new Date().getHours())}, ${builder.display_name || builder.github_login || 'builder'}`;
|
|
645
|
+
}
|
|
646
|
+
if (!sub) return;
|
|
647
|
+
const parts = [];
|
|
648
|
+
const v = currentVersion(ctx.lastProgressState);
|
|
649
|
+
if (v) parts.push(`${v.version_id} is ${Math.round(Number(v.percent_complete) || 0)} percent shipped.`);
|
|
650
|
+
const n = ctx.lastClaimableCount;
|
|
651
|
+
if (typeof n === 'number') {
|
|
652
|
+
if (n === 0) parts.push('Nothing is claimable right now.');
|
|
653
|
+
else {
|
|
654
|
+
const word = n < SMALL_NUMBERS.length ? SMALL_NUMBERS[n] : fmtNum(n);
|
|
655
|
+
parts.push(`${word.charAt(0).toUpperCase()}${word.slice(1)} ${n === 1 ? 'task is' : 'tasks are'} claimable right now.`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
sub.textContent = parts.join(' ');
|
|
659
|
+
sub.hidden = parts.length === 0;
|
|
569
660
|
}
|
|
570
661
|
const ONBOARDING_STAGE_ORDER = [
|
|
571
662
|
'auth_complete',
|
|
@@ -1116,6 +1207,6 @@
|
|
|
1116
1207
|
return idx === -1 ? null : { place: idx + 1, total: ranked.length };
|
|
1117
1208
|
}
|
|
1118
1209
|
|
|
1119
|
-
return { SETUP_CONFIG_KEY, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey };
|
|
1210
|
+
return { SETUP_CONFIG_KEY, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey };
|
|
1120
1211
|
};
|
|
1121
1212
|
})();
|
|
@@ -34,7 +34,17 @@
|
|
|
34
34
|
it sets document.title + this element's text from the branding pack.
|
|
35
35
|
The visible brand mark lives in the sidebar (shell.js reads the pack
|
|
36
36
|
directly); this stays as the invisible chrome hook + a11y page title. -->
|
|
37
|
-
|
|
37
|
+
<!-- task 1003464: the mock opens with a greeting, and it IS the page's one h1
|
|
38
|
+
(one-header-main-h1 — never a second one). The static text is the no-JS /
|
|
39
|
+
signed-out fallback and what brand-apply.js writes the hall's name into;
|
|
40
|
+
hall-render.js renderGreeting turns it into "Good morning, <name>" once
|
|
41
|
+
/me answers, and fills the sub-line — the live status sentence (how far
|
|
42
|
+
the version is, whether anything is claimable) — hidden until it has
|
|
43
|
+
something to say. The label voice is the board's own (.page-head--board). -->
|
|
44
|
+
<div class="page-head page-head--board page-head--hall">
|
|
45
|
+
<h1 id="hall-h1" class="page-head__h">Builders' Hall</h1>
|
|
46
|
+
<p id="hall-sub" class="page-head__sub" hidden></p>
|
|
47
|
+
</div>
|
|
38
48
|
|
|
39
49
|
<!-- BV1.R75 (ADR 0062 §4): hall widgets are NOT hardcoded here. Every widget
|
|
40
50
|
— core (hall-widgets.js) and module-contributed (a module's own client
|
|
@@ -926,11 +926,13 @@ main:focus { outline: none; }
|
|
|
926
926
|
}
|
|
927
927
|
|
|
928
928
|
.app-views .view > .view-zone + .view-zone { margin-top: var(--sp-md); }
|
|
929
|
+
/* No align-items (task 1003464): the mock's .band declares none, so the grid
|
|
930
|
+
default — stretch — makes both cards fill the row and end level. `start` let
|
|
931
|
+
each card shrink to its content, and Standing stopped short under Goals. */
|
|
929
932
|
.view-zone--band {
|
|
930
933
|
display: grid;
|
|
931
934
|
grid-template-columns: 1fr 1fr;
|
|
932
935
|
gap: var(--sp-md);
|
|
933
|
-
align-items: start;
|
|
934
936
|
}
|
|
935
937
|
.view-zone--band > .scroll { margin: 0; }
|
|
936
938
|
.view-zone:empty { display: none; }
|
|
@@ -2782,6 +2784,73 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
2782
2784
|
.ginbox__note { margin: 3px 0 0; font-size: 12.5px; color: var(--ink-soft); font-style: italic; }
|
|
2783
2785
|
.ginbox__actions { flex: 0 0 auto; display: flex; gap: 6px; }
|
|
2784
2786
|
.ginbox__pending { font-size: 12.5px; margin: 10px 0 0; color: var(--ink-soft); }
|
|
2787
|
+
/* task 1003464: the system's notices are inbox LINES on Home (rebase owed, an
|
|
2788
|
+
input the system needs, the monthly budget) — the placement rule, not a
|
|
2789
|
+
banner inside the Standing card. A row is a kind in the label voice over one
|
|
2790
|
+
line of ink; tone marks the exception, on the kind only. */
|
|
2791
|
+
.ginbox__kind {
|
|
2792
|
+
font-family: var(--label-face);
|
|
2793
|
+
font-size: var(--label-size);
|
|
2794
|
+
letter-spacing: var(--label-space);
|
|
2795
|
+
text-transform: var(--label-case);
|
|
2796
|
+
color: var(--ink-faint);
|
|
2797
|
+
}
|
|
2798
|
+
.ginbox__line { font-size: 13.5px; color: var(--ink); }
|
|
2799
|
+
/* a task ref inside a line (rebase owed) is a link in prose: the same 24px
|
|
2800
|
+
row recipe as the row links below, or it sits under the floor at 13.5px */
|
|
2801
|
+
.ginbox__line a {
|
|
2802
|
+
display: inline-block;
|
|
2803
|
+
min-height: 24px;
|
|
2804
|
+
padding: 3px 0;
|
|
2805
|
+
line-height: 18px;
|
|
2806
|
+
box-sizing: border-box;
|
|
2807
|
+
color: var(--ink);
|
|
2808
|
+
text-decoration: underline;
|
|
2809
|
+
text-underline-offset: 3px;
|
|
2810
|
+
}
|
|
2811
|
+
.ginbox__item--warn .ginbox__kind { color: var(--warn); }
|
|
2812
|
+
.ginbox__item--danger .ginbox__kind { color: var(--danger); }
|
|
2813
|
+
/* The link in a Home row takes the mock's row recipe (.att__line a, .g__t): 3px
|
|
2814
|
+
of vertical padding lifts a 13–14px line to the 24px floor without re-spacing
|
|
2815
|
+
the row. The populated harness measured these at 21–23px (task 1003464); the
|
|
2816
|
+
kit's own stub never showed it, because it serves no rows. */
|
|
2817
|
+
.ginbox__goal, .triage-inbox__link, .next-up__title {
|
|
2818
|
+
display: inline-block;
|
|
2819
|
+
min-height: 24px;
|
|
2820
|
+
padding: 3px 0;
|
|
2821
|
+
line-height: 18px;
|
|
2822
|
+
box-sizing: border-box;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/* ---------- Home: the Standing card as the mock draws it (task 1003464) ----------
|
|
2826
|
+
docs/design/mocks/hall/board/v3.html: an identity LINE (a 36px avatar, the
|
|
2827
|
+
name over the handle, the rank on the right) and a three-row fact ledger —
|
|
2828
|
+
Credits, On the board, Preferred work. With .view-zone--band stretching its
|
|
2829
|
+
two cards, this content is what makes Standing end level with Goals. */
|
|
2830
|
+
.home-who { display: flex; align-items: center; gap: 12px; padding-bottom: 14px; }
|
|
2831
|
+
.home-who__avatar {
|
|
2832
|
+
width: 36px; height: 36px; flex: none;
|
|
2833
|
+
border-radius: 50%;
|
|
2834
|
+
background: var(--skeleton);
|
|
2835
|
+
border: 1px solid var(--rule);
|
|
2836
|
+
object-fit: cover;
|
|
2837
|
+
}
|
|
2838
|
+
.home-who__meta { display: flex; flex-direction: column; min-width: 0; }
|
|
2839
|
+
.home-who__name { margin: 0; font-size: 15px; font-weight: 500; color: var(--ink); }
|
|
2840
|
+
.home-who__handle { margin: 0; font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); }
|
|
2841
|
+
.home-who__rank { margin-left: auto; }
|
|
2842
|
+
.home-facts { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 0; }
|
|
2843
|
+
.home-facts > div { display: contents; }
|
|
2844
|
+
.home-facts dt, .home-facts dd { margin: 0; padding: 9px 0; border-top: 1px solid var(--rule-soft); }
|
|
2845
|
+
.home-facts dt {
|
|
2846
|
+
font-family: var(--label-face);
|
|
2847
|
+
font-size: var(--label-size);
|
|
2848
|
+
letter-spacing: var(--label-space);
|
|
2849
|
+
text-transform: var(--label-case);
|
|
2850
|
+
color: var(--ink-faint);
|
|
2851
|
+
}
|
|
2852
|
+
.home-facts dd { text-align: right; font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; }
|
|
2853
|
+
.home-facts > div:first-child dt, .home-facts > div:first-child dd { border-top: 0; padding-top: 0; }
|
|
2785
2854
|
|
|
2786
2855
|
/* ---------- hall-kit components (task 2166) ----------
|
|
2787
2856
|
Styles for the shared kit (hall-kit.js / window.OTBKit): pager bar,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.483",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.483",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.483",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -34,7 +34,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
34
34
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
35
35
|
// the entry to that file. Look for a version's history there, not here.
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
|
-
const CORE_VERSION = '1.19.
|
|
37
|
+
const CORE_VERSION = '1.19.483'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
38
38
|
|
|
39
39
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
40
40
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -220,3 +220,202 @@ test('the link a list writes carries WHERE it was opened from — path, query an
|
|
|
220
220
|
sandbox.location.hash = '#backlog';
|
|
221
221
|
assert.equal(api.href('7'), '/task/7?from=%2Fwork%23backlog', 'on the builders host the hall is mounted at /');
|
|
222
222
|
});
|
|
223
|
+
|
|
224
|
+
// ---- 8. Home matches its board mock (task 1003464) ---------------------------
|
|
225
|
+
//
|
|
226
|
+
// The owner compared live Home against docs/design/mocks/hall/board/v3.html and
|
|
227
|
+
// found four gaps the floors cannot see: the greeting was never built (the page
|
|
228
|
+
// opened cold on a row of numbers, its h1 sr-only), the band ended ragged
|
|
229
|
+
// (`align-items: start` let Standing shrink under Goals), the Standing card had
|
|
230
|
+
// no fact ledger, and the system banners stacked inside it against the
|
|
231
|
+
// placement rule. A floor measures overflow and contrast; none compares a page
|
|
232
|
+
// to the mock it was built from — so these pins do, statically and then by
|
|
233
|
+
// RUNNING hall-render.js against a stub DOM.
|
|
234
|
+
|
|
235
|
+
const indexHtml = read('index.html');
|
|
236
|
+
const hallRenderJs = read('hall-render.js');
|
|
237
|
+
const buildersJs = read('builders.js');
|
|
238
|
+
const goalInboxJs = read('goal-inbox.js');
|
|
239
|
+
|
|
240
|
+
test('the greeting IS the page\'s one h1, in the board\'s label voice, with a sub-line beside it', () => {
|
|
241
|
+
const h1s = indexHtml.match(/<h1\b/g) || [];
|
|
242
|
+
assert.equal(h1s.length, 1, 'one-header-main-h1: the greeting must BECOME the h1, never add a second');
|
|
243
|
+
assert.match(indexHtml, /<div class="page-head page-head--board page-head--hall">\s*<h1 id="hall-h1" class="page-head__h">Builders' Hall<\/h1>\s*<p id="hall-sub" class="page-head__sub" hidden><\/p>/,
|
|
244
|
+
'the h1 is visible (no sr-only), keeps the id brand-apply.js writes into, and the sub-line ships hidden');
|
|
245
|
+
assert.doesNotMatch(indexHtml, /id="hall-h1" class="sr-only"/, 'the sr-only h1 is gone');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('the band ends level: no align-items on the grid, so the two cards stretch', () => {
|
|
249
|
+
const band = rule(styleCss, '.view-zone--band');
|
|
250
|
+
assert.ok(band, '.view-zone--band has a rule');
|
|
251
|
+
assert.doesNotMatch(band, /align-items/, 'the mock\'s .band declares none — stretch is what makes Standing end where Goals ends');
|
|
252
|
+
assert.match(styleCss.replace(/\/\*[\s\S]*?\*\//g, ''), /@media \(max-width:\s*768px\)[\s\S]*?\.view-zone--band \{ grid-template-columns: 1fr; \}/,
|
|
253
|
+
'and the single-column fallback still exists below 768px (the task text said 860; the sheet says 768)');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('the Standing card is the mock\'s: an identity line and a three-row fact ledger, no banner', () => {
|
|
257
|
+
const block = hallRenderJs.match(/const homeStanding = document\.getElementById\('home-standing-body'\);[\s\S]*?renderSystemNotices\(/);
|
|
258
|
+
assert.ok(block, 'the Home card render is where it was');
|
|
259
|
+
assert.match(block[0], /class="home-who"/, 'the identity line');
|
|
260
|
+
assert.match(block[0], /<dt>\$\{escapeHtml\(curLabel\(true\)\)\}<\/dt>/,
|
|
261
|
+
'the currency row takes its label from the branding contract at render time — a data-currency-label attribute is rebranded once at load, before this row exists');
|
|
262
|
+
for (const label of ['On the board', 'Preferred work']) {
|
|
263
|
+
assert.match(block[0], new RegExp(`<dt>${label}</dt>`), `the ledger row: ${label}`);
|
|
264
|
+
}
|
|
265
|
+
assert.match(block[0], /: PREFS_EMPTY\}/, 'the empty-preference copy is the one string both Standing surfaces share');
|
|
266
|
+
assert.doesNotMatch(block[0], /Banner\}/, 'no banner is stacked into the card any more');
|
|
267
|
+
for (const sel of ['.home-who', '.home-facts', '.home-facts dt', '.home-facts dd']) {
|
|
268
|
+
assert.ok(rule(styleCss, sel), `style.css styles ${sel}`);
|
|
269
|
+
}
|
|
270
|
+
assert.match(rule(styleCss, '.home-who__avatar') || '', /var\(--skeleton\)/, 'the avatar placeholder is the skeleton tone, not --bg-deep (pure black in dark)');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('the system notices are inbox LINES, first in the card, and counted by its refresh()', () => {
|
|
274
|
+
const mount = goalInboxJs.match(/host\.innerHTML =([\s\S]*?);/);
|
|
275
|
+
assert.ok(mount, 'the inbox mounts its slices');
|
|
276
|
+
const sys = mount[1].indexOf('id="attention-system"');
|
|
277
|
+
const goals = mount[1].indexOf('id="attention-goals"');
|
|
278
|
+
assert.ok(sys > -1 && sys < goals, '#attention-system is a slice, ahead of the goals slice');
|
|
279
|
+
assert.match(hallRenderJs, /function renderSystemNotices\(/, 'hall-render.js renders them from the /me state it already holds');
|
|
280
|
+
const fn = hallRenderJs.match(/function renderSystemNotices\([\s\S]*?\n \}\n/);
|
|
281
|
+
assert.ok(fn, 'renderSystemNotices is a whole function');
|
|
282
|
+
assert.match(fn[0], /class="ginbox__item ginbox__item--/, 'rows carry .ginbox__item, so OTBAttention.refresh() counts them');
|
|
283
|
+
assert.match(fn[0], /OTBAttention[\s\S]*refresh\(\)/, 'and the renderer asks the card to re-decide its visibility');
|
|
284
|
+
assert.doesNotMatch(fn[0], /covered/, 'a "we cover it" note is information, not an action, and does not land in the inbox');
|
|
285
|
+
// the rebase-owed line carries task refs as links in prose; a 13.5px link
|
|
286
|
+
// with no compensation sits under the 24px floor, and the kit stub renders
|
|
287
|
+
// no notices, so this is pinned in the sheet rather than left to a render
|
|
288
|
+
for (const sel of ['.ginbox__line a', '.ginbox__goal, .triage-inbox__link, .next-up__title']) {
|
|
289
|
+
const r = rule(styleCss, sel) || '';
|
|
290
|
+
assert.match(r, /min-height:\s*24px/, `${sel} clears the floor`);
|
|
291
|
+
assert.match(r, /padding:\s*3px 0/, `${sel} buys it with the row recipe`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('the greeting reads /me, the public progress and the claimable feed, and builders.js feeds all three', () => {
|
|
296
|
+
assert.match(buildersJs, /getJSON\(`\$\{API\}\/public\/progress`\)/, 'Home fetches the public progress once');
|
|
297
|
+
assert.match(buildersJs, /lastClaimableCount = resp && Array\.isArray\(resp\.claimable\) \? resp\.claimable\.length : null;/,
|
|
298
|
+
'the claimable feed Home already fetches for Next-up answers the count');
|
|
299
|
+
assert.match(buildersJs, /get lastProgressState\(\)/, 'the renderer reads progress through a getter, so it cannot freeze at boot');
|
|
300
|
+
assert.match(buildersJs, /get lastClaimableCount\(\)/, 'and the count the same way');
|
|
301
|
+
assert.equal((buildersJs.match(/renderGreeting\(\)/g) || []).length, 3, 'rendered after /me, after progress and after the claimable feed — each half as it lands');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Boot hall-render.js against a stub DOM and a ctx of getters (the way
|
|
305
|
+
// builders.js builds it) and read what the greeting and the card actually say.
|
|
306
|
+
function bootHallRender({ me, progress = null, claimable = null, leaderboard = [] }) {
|
|
307
|
+
const els = new Map();
|
|
308
|
+
const makeEl = () => ({
|
|
309
|
+
dataset: {}, style: {}, textContent: '', hidden: false, _html: '', innerHTML: '',
|
|
310
|
+
classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
|
|
311
|
+
setAttribute() {}, getAttribute() { return null; }, addEventListener() {},
|
|
312
|
+
querySelector() { return makeEl(); }, querySelectorAll() { return []; }, appendChild() {}, remove() {},
|
|
313
|
+
});
|
|
314
|
+
const documentObj = {
|
|
315
|
+
getElementById(id) { if (!els.has(id)) els.set(id, makeEl()); return els.get(id); },
|
|
316
|
+
querySelector(sel) { return this.getElementById(String(sel).replace(/^#/, '')); },
|
|
317
|
+
querySelectorAll() { return []; },
|
|
318
|
+
createElement() { return makeEl(); },
|
|
319
|
+
addEventListener() {},
|
|
320
|
+
body: { dataset: {} }, documentElement: { dataset: {} },
|
|
321
|
+
};
|
|
322
|
+
const escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
323
|
+
const windowObj = {
|
|
324
|
+
OTB: { escapeHtml, $: (sel) => documentObj.querySelector(sel), fmtNum: (n) => Number(n || 0).toLocaleString('en-US'), moduleOn: () => false },
|
|
325
|
+
// the factory builds the Standing ships pager + rail at creation
|
|
326
|
+
OTBKit: { makePager: () => ({ page: 1, perPage: 10 }), makeSectionRail: () => ({ render() {}, refresh() {} }), paginate: (rows) => rows, shipVisualThumbHtml: () => '' },
|
|
327
|
+
OTBTaskDetail: { href: (id) => `/task/${id}?from=%2F` },
|
|
328
|
+
location: { hostname: 'builders.example.com', pathname: '/', hash: '', href: 'https://builders.example.com/' },
|
|
329
|
+
localStorage: { getItem() { return null; }, setItem() {}, removeItem() {} },
|
|
330
|
+
addEventListener() {},
|
|
331
|
+
};
|
|
332
|
+
const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, localStorage: windowObj.localStorage, console, setTimeout, clearTimeout, Date, URL };
|
|
333
|
+
sandbox.globalThis = sandbox;
|
|
334
|
+
vm.createContext(sandbox);
|
|
335
|
+
vm.runInContext(read('hall-render.js'), sandbox, { filename: 'hall-render.js' });
|
|
336
|
+
const ctx = {
|
|
337
|
+
API: '/api/gds', RANK_ORDER: ['xenos', 'thetes', 'metic', 'archon'], api: {}, brandWorld: () => 'Cloud Bongos',
|
|
338
|
+
curLabel: (caps) => (caps ? 'Credits' : 'credits'), emptyRow: () => '', fmtDate: (d) => String(d).slice(0, 10),
|
|
339
|
+
mins: (m) => `${m}m`, normalizeRank: (r) => String(r || '').toLowerCase(), pct: (n) => `${n}%`, postJSON: async () => ({}),
|
|
340
|
+
rankLabel: (r) => r, rankWeight: () => 0, signClasses: () => '', toast() {}, usd: (n) => `$${n}`,
|
|
341
|
+
instanceRepo: null, newcomerGated: false,
|
|
342
|
+
get lastMeState() { return me; }, get lastCostState() { return me.cost || null; }, get lastKarmaState() { return me.karma || null; },
|
|
343
|
+
get lastNeedsState() { return me.needs || null; }, get lastRebaseState() { return me.needs_rebase_tasks || null; },
|
|
344
|
+
get lastProgressState() { return progress; }, get lastClaimableCount() { return claimable; },
|
|
345
|
+
};
|
|
346
|
+
const api = windowObj.OTBHallRender(ctx);
|
|
347
|
+
api.lbState.rows = leaderboard;
|
|
348
|
+
return { api, el: (id) => documentObj.getElementById(id) };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const ME = {
|
|
352
|
+
builder: { id: 7, display_name: 'Percy Jackson', github_login: 'percy', rank: 'metic', total_credits: 14441, karma: 96, preferred_disciplines: [], created_at: '2026-01-01T00:00:00Z' },
|
|
353
|
+
needs: { items: [{ state: 'action_needed', title: 'Image-generation key', message: 'Your free art coverage has ended.', action: { href: '/settings#art', label: 'Add your key' } }] },
|
|
354
|
+
cost: { level: 'warn', spend_mtd_usd: 80, budget_mtd_usd: 100, pct_used: 80 },
|
|
355
|
+
needs_rebase_tasks: [{ id: 1003460 }],
|
|
356
|
+
};
|
|
357
|
+
const LEADERBOARD = [{ github_login: 'a', total_credits: 20000 }, { github_login: 'percy', total_credits: 14441 }, { github_login: 'b', total_credits: 3 }, { github_login: 'c', total_credits: 1 }];
|
|
358
|
+
const PROGRESS = { progress: [{ version_id: 'BONGOS-V1', status: 'building', total_count: 605, percent_complete: 75 }, { version_id: 'V2', status: 'planning', total_count: 9000, percent_complete: 0 }] };
|
|
359
|
+
|
|
360
|
+
test('RUN: the greeting names the builder, and the sub-line says how far the version is and what is claimable', () => {
|
|
361
|
+
const { api, el } = bootHallRender({ me: ME, progress: PROGRESS, claimable: 5 });
|
|
362
|
+
api.renderGreeting();
|
|
363
|
+
assert.match(el('hall-h1').textContent, /^Good (morning|afternoon|evening), Percy Jackson$/);
|
|
364
|
+
assert.equal(el('hall-sub').textContent, 'BONGOS-V1 is 75 percent shipped. Five tasks are claimable right now.');
|
|
365
|
+
assert.equal(el('hall-sub').hidden, false);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('RUN: each half of the sentence renders as its data lands, and the line hides with neither', () => {
|
|
369
|
+
const none = bootHallRender({ me: ME });
|
|
370
|
+
none.api.renderGreeting();
|
|
371
|
+
assert.equal(none.el('hall-sub').hidden, true, 'nothing to say yet');
|
|
372
|
+
const onlyProgress = bootHallRender({ me: ME, progress: PROGRESS });
|
|
373
|
+
onlyProgress.api.renderGreeting();
|
|
374
|
+
assert.equal(onlyProgress.el('hall-sub').textContent, 'BONGOS-V1 is 75 percent shipped.');
|
|
375
|
+
const zero = bootHallRender({ me: ME, progress: PROGRESS, claimable: 0 });
|
|
376
|
+
zero.api.renderGreeting();
|
|
377
|
+
assert.match(zero.el('hall-sub').textContent, /Nothing is claimable right now\.$/);
|
|
378
|
+
const one = bootHallRender({ me: ME, claimable: 1 });
|
|
379
|
+
one.api.renderGreeting();
|
|
380
|
+
assert.equal(one.el('hall-sub').textContent, 'One task is claimable right now.');
|
|
381
|
+
const many = bootHallRender({ me: ME, claimable: 63 });
|
|
382
|
+
many.api.renderGreeting();
|
|
383
|
+
assert.equal(many.el('hall-sub').textContent, '63 tasks are claimable right now.');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('RUN: the Standing card carries the three facts, the place as "2nd of 4", and no banner', () => {
|
|
387
|
+
const { api, el } = bootHallRender({ me: ME, leaderboard: LEADERBOARD });
|
|
388
|
+
api.renderProfile(ME.builder);
|
|
389
|
+
const html = el('home-standing-body').innerHTML;
|
|
390
|
+
assert.match(html, /<p class="home-who__name">Percy Jackson<\/p>/);
|
|
391
|
+
assert.match(html, /<p class="home-who__handle">@percy<\/p>/);
|
|
392
|
+
// written from curLabel(true), not a data-currency-label attribute: brand-apply
|
|
393
|
+
// rebrands those once at load, and this row is stamped after /me answers
|
|
394
|
+
assert.match(html, /<dt>Credits<\/dt><dd>14,441<\/dd>/);
|
|
395
|
+
assert.match(html, /<dt>On the board<\/dt><dd>2nd of 4<\/dd>/);
|
|
396
|
+
assert.match(html, /<dt>Preferred work<\/dt><dd>Not set yet<\/dd>/, 'the mock\'s copy for an empty preference');
|
|
397
|
+
assert.doesNotMatch(html, /profile__(needs|budget|rebase)/, 'the banners are not in the card');
|
|
398
|
+
const withPrefs = bootHallRender({ me: ME, leaderboard: [] });
|
|
399
|
+
withPrefs.api.renderProfile({ ...ME.builder, preferred_disciplines: ['engineer', 'ui'] });
|
|
400
|
+
assert.match(withPrefs.el('home-standing-body').innerHTML, /<dd>engineer, ui<\/dd>/);
|
|
401
|
+
assert.match(withPrefs.el('home-standing-body').innerHTML, /<dt>On the board<\/dt><dd>—<\/dd>/, 'no roll yet: a dash, not a wrong place');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('RUN: the three notices are inbox lines with a kind, a line and — where one exists — a door', () => {
|
|
405
|
+
const { api, el } = bootHallRender({ me: ME });
|
|
406
|
+
api.renderProfile(ME.builder);
|
|
407
|
+
const html = el('attention-system').innerHTML;
|
|
408
|
+
assert.match(html, /<h3 class="ginbox__sec-h">From the system<\/h3>/);
|
|
409
|
+
assert.equal((html.match(/class="ginbox__item /g) || []).length, 3, 'rebase, the needed input, the budget');
|
|
410
|
+
assert.match(html, /ginbox__item--danger"><div class="ginbox__body"><span class="ginbox__kind">Rebase owed<\/span>/);
|
|
411
|
+
assert.match(html, /<a href="\/task\/1003460\?from=%2F">task 1003460<\/a>/, 'a task ref is a door through OTBTaskDetail.href, never a bare #N');
|
|
412
|
+
assert.match(html, /ginbox__item--warn"><div class="ginbox__body"><span class="ginbox__kind">Image-generation key<\/span>/);
|
|
413
|
+
assert.match(html, /<a class="btn-ghost" href="\/settings#art">Add your key<\/a>/);
|
|
414
|
+
assert.match(html, /<span class="ginbox__kind">Approaching monthly budget<\/span>/);
|
|
415
|
+
const quiet = bootHallRender({ me: { builder: ME.builder } });
|
|
416
|
+
quiet.api.renderProfile(ME.builder);
|
|
417
|
+
assert.equal(quiet.el('attention-system').innerHTML, '', 'nothing owed, nothing rendered');
|
|
418
|
+
const covered = bootHallRender({ me: { builder: ME.builder, needs: { items: [{ state: 'covered', title: 'Art', message: 'We cover it.' }] } } });
|
|
419
|
+
covered.api.renderProfile(ME.builder);
|
|
420
|
+
assert.equal(covered.el('attention-system').innerHTML, '', 'a covered need is not an action');
|
|
421
|
+
});
|