mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
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:
@@ -377,9 +377,21 @@ function digestRunningHead(dateShort, label) {
|
|||||||
// ── Page renderers ───────────────────────────────────────────────────────────
|
// ── 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 =
|
const blurb =
|
||||||
storyCount === 1
|
storyCount === 1
|
||||||
? 'One thread that shaped the world today.'
|
? 'One thread that shaped the world today.'
|
||||||
@@ -399,7 +411,7 @@ function renderCover({ dateLong, issue, storyCount, pageIndex, totalPages }) {
|
|||||||
`<p class="blurb">${escapeHtml(blurb)}</p>` +
|
`<p class="blurb">${escapeHtml(blurb)}</p>` +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="meta-bottom">' +
|
'<div class="meta-bottom">' +
|
||||||
'<span class="mono">Good evening</span>' +
|
`<span class="mono">${escapeHtml(coverGreeting(greeting))}</span>` +
|
||||||
'<span class="mono">Swipe / ↔ to begin</span>' +
|
'<span class="mono">Swipe / ↔ to begin</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
|
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
|
||||||
@@ -1200,6 +1212,7 @@ export function renderBriefMagazine(envelope, options = {}) {
|
|||||||
storyCount: stories.length,
|
storyCount: stories.length,
|
||||||
pageIndex: ++p,
|
pageIndex: ++p,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
greeting: digest.greeting,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -700,3 +700,67 @@ describe('renderBriefMagazine — publicMode', () => {
|
|||||||
assert.equal(a, b);
|
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('<script>'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user