DocumentsLobeHub
Changelog Display
Changelog Display
Type
Topic
Status
Published
Created
Jul 7, 2026
Updated
Jul 7, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

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: ChangelogService depends on gray-matter (which calls Node.js Buffer) 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 :

  1. If no currentId prop was passed in, it calls lambdaClient.changelog.getIndex.query() and sets currentId to index[0].id (the most-recent changelog entry).

After currentId resolves :

  • Waits 1 second (debounce timer) then checks latestChangelogId from the global store:
    • latestChangelogId is unset → first-ever visit; silently records currentId as the new baseline with updateSystemStatus({ latestChangelogId: currentId }).
    • latestChangelogId !== currentId → new release available; navigates to /changelog/modal to 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.

latestChangelogId is intentionally absent from INITIAL_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):

ProcedureInputCalls
getIndexChangelogService.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#

FilePurpose
src/features/ChangelogModal/index.tsxClient trigger component; decides whether to open modal
src/features/ChangelogModal/ChangelogContent.tsxRenders the changelog post body via tRPC
src/server/routers/lambda/changelog.tstRPC server router; getIndex + getPostById
src/server/services/Changelog/Server-only MDX parser using gray-matter
src/store/global/initialState.tsSystemStatus.latestChangelogId type definition
src/store/global/updateSystemStatus action; writes to AsyncLocalStorage

Common Debugging Points#

  • Modal never appears — check LOBE_SYSTEM_STATUS in localStorage; if latestChangelogId matches the server's latest ID, the guard suppresses it. Clear or update the key to force the modal.
  • Buffer is not defined error — a server-only import (e.g. ChangelogService) leaked into a 'use client' component. All changelog data access from the browser must go through lambdaClient.changelog.*. See PR #14435 for the canonical fix.
  • Modal shows on every loadupdateSystemStatus may not be persisting; verify the global store's AsyncLocalStorage flush is completing before the user navigates away.
Changelog Display | Dosu