Changelog Display#
The changelog display system fetches, shows, and persists new-release notices as a modal. It follows a strict client/server split: a tRPC server router reads MDX files and parses frontmatter on the Node.js side, while a lightweight client component consults localStorage to decide whether to surface the modal.
Architecture Overview#
Browser Server (Node.js / tRPC)
─────────────────────────────── ────────────────────────────────────
ChangelogModal (client component)
│
├─ reads latestChangelogId ──────▶ AsyncLocalStorage (LOBE_SYSTEM_STATUS)
│
└─ lambdaClient.changelog ───────▶ changelogRouter
.getIndex.query() getIndex → ChangelogService.getChangelogIndex()
.getPostById.query(id, locale) getPostById → ChangelogService.getPostById()
Key constraint:
ChangelogServicedepends ongray-matter(which calls Node.jsBuffer) and must never run in the browser. All data fetching goes through tRPC. See PR #14435 for the history of this split.
Entry Point: ChangelogModal#
src/features/ChangelogModal/index.tsx is a 'use client' component that renders null — it only drives navigation side-effects.
On mount :
- If no
currentIdprop was passed in, it callslambdaClient.changelog.getIndex.query()and setscurrentIdtoindex[0].id(the most-recent changelog entry).
After currentId resolves :
- Waits 1 second (debounce timer) then checks
latestChangelogIdfrom the global store:latestChangelogIdis unset → first-ever visit; silently recordscurrentIdas the new baseline withupdateSystemStatus({ latestChangelogId: currentId }).latestChangelogId !== currentId→ new release available; navigates to/changelog/modalto open the modal.latestChangelogId === currentId→ already seen; does nothing.
Persisted State: latestChangelogId#
latestChangelogId is a field on the SystemStatus interface stored in status inside GlobalState. The store flushes SystemStatus to AsyncLocalStorage under the key LOBE_SYSTEM_STATUS . This means the "seen" changelog ID survives page reloads without requiring a logged-in user or a server round-trip.
latestChangelogIdis intentionally absent fromINITIAL_STATUS— an undefined value is the sentinel that distinguishes a brand-new installation from a returning user who simply hasn't seen the latest release.
Server Router: changelogRouter#
Located at src/server/routers/lambda/changelog.ts (added in PR #14435), the router exposes two publicProcedure queries (no auth required):
| Procedure | Input | Calls |
|---|---|---|
getIndex | — | ChangelogService.getChangelogIndex() |
getPostById | { id: string, locale?: string } | ChangelogService.getPostById(id, { locale }) |
The router is registered as changelog: changelogRouter inside the main lambdaRouter . Failures are wrapped in TRPCError(INTERNAL_SERVER_ERROR, ...).
Content Rendering: ChangelogContent#
src/features/ChangelogModal/ChangelogContent.tsx is a 'use client' component that uses SWR to call lambdaClient.changelog.getPostById.query({ id, locale }) and render the fetched MDX post body inside the modal. The navigation destination /changelog/modal is where this component lives. See PR #14435 for how its earlier direct ChangelogService import was removed to fix browser-incompatible Buffer errors.
Key Files#
| File | Purpose |
|---|---|
src/features/ChangelogModal/index.tsx | Client trigger component; decides whether to open modal |
src/features/ChangelogModal/ChangelogContent.tsx | Renders the changelog post body via tRPC |
src/server/routers/lambda/changelog.ts | tRPC server router; getIndex + getPostById |
src/server/services/Changelog/ | Server-only MDX parser using gray-matter |
src/store/global/initialState.ts | SystemStatus.latestChangelogId type definition |
src/store/global/ | updateSystemStatus action; writes to AsyncLocalStorage |
Common Debugging Points#
- Modal never appears — check
LOBE_SYSTEM_STATUSinlocalStorage; iflatestChangelogIdmatches the server's latest ID, the guard suppresses it. Clear or update the key to force the modal. Buffer is not definederror — a server-only import (e.g.ChangelogService) leaked into a'use client'component. All changelog data access from the browser must go throughlambdaClient.changelog.*. See PR #14435 for the canonical fix.- Modal shows on every load —
updateSystemStatusmay not be persisting; verify the global store'sAsyncLocalStorageflush is completing before the user navigates away.