fix(brief): cover greeting was hardcoded "Good evening" regardless of issue time (#3254)

* fix(brief): cover greeting was hardcoded "Good evening" regardless of issue time

Reported: a brief viewed at 13:02 local time showed "Good evening" on the
cover (slide 1) but "Good afternoon." on the digest greeting page (slide 2).

Cause: `server/_shared/brief-render.js:renderCover` had the string
`'Good evening'` hardcoded in the cover's mono-cased salutation slot.
The digest greeting page (slide 2) renders the time-of-day-correct
value from `envelope.data.digest.greeting`, which is computed by
`shared/brief-filter.js:174-179` from `localHour` in the user's TZ
(< 12 → morning, < 18 → afternoon, else → evening). So any brief
viewed outside the literal evening showed an inconsistent pair.

Fix: thread `digest.greeting` into `renderCover`; a small
`coverGreeting()` helper strips the trailing period so the cover's
no-punctuation mono style is preserved. On unexpected/missing values
it falls back to a generic "Hello" rather than silently re-hardcoding
a specific time of day.

Tests: 5 regression cases in `tests/brief-magazine-render.test.mjs`
cover afternoon/morning/evening parity, period stripping, and HTML
escape (defense-in-depth). 60 total in that file pass. Full
test:data 5921 pass. typecheck + typecheck:api + biome clean.

* chore(brief): fix orphaned JSDoc on coverGreeting / renderCover

Greptile flagged: the original `renderCover` JSDoc block stayed above
`coverGreeting` when the helper was inserted, so the @param shape was
misattributed to the wrong function and `renderCover` was left
undocumented (plus the new `greeting` field was unlisted).

Moved the opts-shape JSDoc to immediately above `renderCover` and
added `greeting: string` to the param type. `coverGreeting` keeps its
own prose comment.

No runtime change.
This commit is contained in:
Elie Habib
2026-04-21 13:46:21 +04:00
committed by GitHub
parent b0928f213c
commit 89c179e412
2 changed files with 80 additions and 3 deletions

View File

@@ -377,9 +377,21 @@ function digestRunningHead(dateShort, label) {
// ── Page renderers ───────────────────────────────────────────────────────────
/**
* @param {{ dateLong: string; issue: string; storyCount: number; pageIndex: number; totalPages: number }} opts
* Strip the trailing period from envelope.data.digest.greeting
* ("Good afternoon." → "Good afternoon") so the cover's mono-cased
* salutation stays consistent with the historical no-period style.
* Defensive: if the envelope ever produces an unexpected value, fall
* back to a generic "Hello" rather than hardcoding a wrong time-of-day.
*/
function renderCover({ dateLong, issue, storyCount, pageIndex, totalPages }) {
function coverGreeting(greeting) {
if (typeof greeting !== 'string' || greeting.length === 0) return 'Hello';
return greeting.replace(/\.+$/, '').trim() || 'Hello';
}
/**
* @param {{ dateLong: string; issue: string; storyCount: number; pageIndex: number; totalPages: number; greeting: string }} opts
*/
function renderCover({ dateLong, issue, storyCount, pageIndex, totalPages, greeting }) {
const blurb =
storyCount === 1
? 'One thread that shaped the world today.'
@@ -399,7 +411,7 @@ function renderCover({ dateLong, issue, storyCount, pageIndex, totalPages }) {
`<p class="blurb">${escapeHtml(blurb)}</p>` +
'</div>' +
'<div class="meta-bottom">' +
'<span class="mono">Good evening</span>' +
`<span class="mono">${escapeHtml(coverGreeting(greeting))}</span>` +
'<span class="mono">Swipe / ↔ to begin</span>' +
'</div>' +
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
@@ -1200,6 +1212,7 @@ export function renderBriefMagazine(envelope, options = {}) {
storyCount: stories.length,
pageIndex: ++p,
totalPages,
greeting: digest.greeting,
}),
);

View File

@@ -700,3 +700,67 @@ describe('renderBriefMagazine — publicMode', () => {
assert.equal(a, b);
});
});
// ── Regression: cover greeting follows envelope.data.digest.greeting ─────────
// Previously the cover hardcoded "Good evening" regardless of issue time, so
// a brief composed at 13:02 local (envelope greeting = "Good afternoon.")
// rendered "Good evening" on the cover and "Good afternoon." on slide 2 —
// visibly inconsistent. Fix wires digest.greeting into the cover (period
// stripped for the mono-cased slot).
describe('cover greeting ↔ digest.greeting parity', () => {
/**
* Extract the cover <section> so we can assert on it in isolation without
* matching the identical greeting that appears on slide 2.
*/
function extractCover(html) {
const match = html.match(/<section class="page cover">[\s\S]*?<\/section>/);
assert.ok(match, 'cover section must be present');
return match[0];
}
it('renders "Good afternoon" on the cover when digest.greeting is "Good afternoon."', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: 'Good afternoon.' },
});
const cover = extractCover(renderBriefMagazine(env));
assert.ok(cover.includes('>Good afternoon<'), `cover should contain "Good afternoon" without period, got: ${cover}`);
assert.ok(!cover.includes('Good evening'), 'cover must NOT say "Good evening" when digest.greeting is afternoon');
});
it('renders "Good morning" on the cover when digest.greeting is "Good morning."', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: 'Good morning.' },
});
const cover = extractCover(renderBriefMagazine(env));
assert.ok(cover.includes('>Good morning<'));
assert.ok(!cover.includes('Good evening'));
assert.ok(!cover.includes('Good afternoon'));
});
it('renders "Good evening" on the cover when digest.greeting is "Good evening."', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: 'Good evening.' },
});
const cover = extractCover(renderBriefMagazine(env));
assert.ok(cover.includes('>Good evening<'));
});
it('strips trailing period(s) — cover is mono-cased, no punctuation', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: 'Good afternoon...' },
});
const cover = extractCover(renderBriefMagazine(env));
// Envelope can send any trailing dot count; cover strips all of them.
assert.ok(cover.includes('>Good afternoon<'));
assert.ok(!cover.includes('Good afternoon.'));
});
it('HTML-escapes the greeting (defense-in-depth, even though envelope values are controlled)', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: '<script>alert(1)</script>.' },
});
const cover = extractCover(renderBriefMagazine(env));
assert.ok(!cover.includes('<script>alert'));
assert.ok(cover.includes('&lt;script&gt;'));
});
});