Blog

Chessolve 6.7.0 is the largest update since the Chess.com RSocket work in 6.6.0

By Chessolve10 min read

Chessolve 6.7.0 is the largest update since the Chess.com RSocket work in 6.6.0.

It adds a fourth panel screen, Notifications, with a persistent unread badge, localized content, and a detail view that can render safe Markdown. It also teaches the Chess.com adapter to read more of the site's own game object, adds Google sign-in, and completes a class-based refactor of the extension's core.

The release is partly visible and partly structural. This post covers both, and the decisions behind them.

Notifications, where you already are

The notification center lives in the bottom navigation between Home and Options. The bell tab carries a badge with the total unread count: the server's unread notifications plus the local update card. The badge shows 1 through 9, then 9+; its accessible label always carries the real number.

The list is an inbox, not a feed of transient toasts:

  • All and Unread filters.
  • Cursor-based infinite scrolling with skeleton rows.
  • A type pill, date, title, and two-line summary on every card.
  • An unread dot that disappears when the notification is opened.
  • Mark all as read, applied optimistically and reconciled with the server.
  • Empty and error states with retry, rather than a blank screen.
  • A full detail view with a cover image, Markdown body, and an optional call-to-action link.

Opening a notification marks it read on the server. If the notification was deleted or expired between the list load and the tap, the detail closes, the row is removed, and the list returns instead of showing a dead end.

The center is intentionally an in-app inbox. It is not an OS-level push notification system. The unread count polls the server every 60 seconds, pauses while the page is hidden, and refreshes immediately when the page becomes visible again. That keeps the badge current without waking a background worker on every tab.

The server side of an inbox

Two collections back the feature:

  • notifications stores the content, type, audience, schedule, and optional cover and CTA.
  • notification-reads stores one receipt per user and notification.

Visibility is a database query, not a cleanup job:

ts
const visibility = {
    draft: { $ne: true },
    publishAt: { $lte: now },
    $and: [
        { $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }] },
        { $or: [{ "audience.kind": "all" }, { "audience.userIds": userId }] },
    ],
};

A scheduled notification becomes visible when publishAt passes. An expired one stops matching when expiresAt passes. A personal notification only matches the targeted user. No background process has to flip a status field, and a failed job cannot leave stale content visible.

The feed is cursor-paginated on publishAt and _id, so new notifications arriving at the top do not shift the page a reader is currently loading. The list payload deliberately omits the body; the detail request fetches it. Bodies are capped at 20,000 characters, which is large enough for a real announcement and small enough to keep a single response bounded.

The model supports four types: system, news, update, and ads. This release exposes All and Unread filters only — no per-type mute preferences, archiving, snoozing, or deletion. Those are reasonable future features, but they are not part of this version.

Read state without fan-out

The tempting shortcut is a readBy array on each notification. It is simple until a global announcement reaches a large audience, at which point every read appends to one document and every list query has to inspect it.

Instead, a read is a separate row with a unique compound index on (userId, notificationId). The feed joins the current user's receipts, derives isRead, and optionally filters to unread rows. Mark-all is idempotent: it reports zero when there is nothing left to mark.

This design has a few useful consequences:

  • A global notification can be edited after people have read it without clearing their receipts.
  • Narrowing and later restoring an audience preserves the read state of users who return.
  • Unread counts are a count over the same visibility query, not a maintained counter that can drift.
  • Per-user copies are never created, so a global send does not multiply storage by the size of the audience.

Localized content and Markdown that cannot become HTML

Notification content is stored per locale: English is required, and the other supported locales are optional. The server resolves the reader's x-client-locale and falls back to English when a translation is missing. The CTA is stored as a label and URL pair, so a half-filled pair becomes no button rather than a button with a missing target.

Bodies use a deliberately small Markdown subset:

  • paragraphs and line breaks;
  • headings, bold, italic, and inline code;
  • links and autolinks with an http, https, or mailto allowlist;
  • ordered and unordered lists, blockquotes, fenced code blocks, and horizontal rules.

Raw HTML is escaped, not rendered. Inline images, tables, and MDX components are unsupported. Links open with target="_blank" and rel="noopener noreferrer nofollow". The renderer is a pure string module with no third-party dependency and no DOM access, which makes its safety properties testable in isolation.

The cover image is a separate field with its own alt text, lazy loading, and referrerpolicy="no-referrer". If the host page's content security policy blocks the image, the image hides and the rest of the layout stays in place.

The underlying rule is simple: notification content is data, not code. A message should never be able to introduce a script, an event handler, or an unexpected navigation target.

Release notes without a banner

The old Home screen version banner is gone, along with the version check that fed it. Release communication now happens in two complementary ways.

First, the server can create an update notification for every user when a new build is published. It uses the built-in update artwork and carries the version number, so it behaves like any other notification: unread until opened, with server-side read state.

Second, the extension keeps a local update card pinned to the top of the list. It reads the latest blog post tagged update, renders the post's title, summary, cover, and Markdown body in the detail view, and marks the post read when opened.

The local card has no server row. Its identity is slug:publishedAt and it lives in chrome.storage.local, which means the same update is marked read across Chess.com, Lichess, and take-take-take. Locale is deliberately excluded from the identity, so reading an update in one language marks it read in all of them. When a newer post tagged update is published, the card becomes unread again; Mark all as read clears it until the next update.

This post carries the update tag, which is what makes a post eligible for that card once it is published.

Chess.com: let the game object speak

The 6.6.0 release established a hierarchy of evidence for live Chess.com games: transport history where it is authoritative, verified board state where it is not, and no answer when neither source can support one safely.

6.7.0 adds another source that was already on the page: the site's own board object.

The bridge now reads the game's headers, playing color, turn, game-over and check state, drag and animation flags, premove count, mode, variant, and starting move number. That is a much richer snapshot than the visible board alone, and it arrives before the sidebar finishes rendering.

The adapter uses it in three places.

The live board FEN. On a live mainline board, the game object's FEN is the position. The adapter returns it directly instead of reconstructing the position from piece elements. The review board keeps the visual check, because a review can sit on a ply the board has not finished drawing yet. The distinction matters: a model that is one render ahead of the pixels must not become the position an analysis pass answers for.

Participant identity. White and Black names, ratings, event, result, and time control come from the game headers. The adapter resolves the local color from playingAs, or by matching the signed-in account name against the headers. That gives the panel the right player perspective before the participant rows exist, and it handles guest and bot games without inventing a site account. The rows remain the fallback when headers are missing.

Move history. The game object exposes its SAN record. The adapter replays that record to UCI, validates each move against the position, and caches the result by initial FEN, move count, and last SAN. That history is preferred over the transport and DOM histories.

Why prefer it? Because the board can already show the local move while the sidebar move list and the server echo are still a ply behind. Replaying the game's own record lets a score request name the position the player is looking at, without waiting for either view to catch up. If the record contains a move that cannot be replayed legally, the adapter discards the replay and falls back to the transport or DOM path.

There is also a small but important interaction fix: the adapter now treats the site's isDragging and isAnimating flags as an interaction lock. An analysis pass no longer starts in the middle of a drag or a board animation just because the extension's own pointer state looks idle.

Finally, bot boards get a narrower commit rule. A move-list node match is not available on every Chess.com surface. When it is absent, a replayed DOM history that matches the displayed board is now accepted as proof of the position; only a match that actually contradicts the board refuses the commit. That removes a class of missing suggestions on bot games without weakening the check on normal games.

Sign in with Google

6.7.0 also adds Google sign-in.

The extension opens Google's OAuth 2.0 consent page through chrome.identity.launchWebAuthFlow, using the authorization-code flow with PKCE and the openid email profile scopes. Google returns a code to the extension's redirect URL. The extension sends that code, its verifier, and the redirect URI to Chessolve's server, which performs the token exchange and creates the session.

The extension never handles the user's Google password and never receives a client secret. The Google account's name, email, and avatar are used to create or identify the Chessolve account and are shown in the welcome and profile surfaces.

This is also the only reason the extension requests the identity permission. It does not read the Chrome profile, browser history, or any other account data.

A class-based core

The visible features sit on top of a structural change. The extension's adapters, bridges, and controllers were refactored into classes with explicit dependencies and lifecycles. The timer service now has its own worker-backed implementation, and screen controllers own their open, close, render, and cleanup paths instead of relying on shared module state.

The refactor was not cosmetic. The notification center adds a global controller, a list controller, and a detail controller, all of which need predictable startup, teardown, polling, and stale-request handling. Having explicit lifecycles made those behaviors testable without a live panel.

The release includes focused coverage for the notification store, pagination, filters, mark-all, Markdown safety, badge rendering, navigation transitions, the Chess.com header and history paths, and the existing game-state pipeline. The goal is not a particular architecture diagram; it is that every asynchronous answer can be tied back to the context that requested it.

What 6.7.0 changes in practice

  • A fourth Notifications tab with an unread badge, All and Unread filters, infinite scrolling, and mark-all.
  • A full notification detail view with localized content, cover images, safe Markdown, and optional links.
  • Release notes that arrive as notifications instead of a Home banner.
  • A local update card that follows the latest blog post tagged update across all supported chess sites.
  • More reliable Chess.com player identity, move history, and interaction handling on live and bot games.
  • Google sign-in alongside the existing sign-in methods.
  • A cleaner, class-based extension core with a larger regression suite.

Chessolve 6.7.0 is available for Chrome and Firefox. If you already have the extension installed, update it and reload the chess site you are using. The notification badge will pick up the new update post on its next count refresh.

Ready to play better chess?

See the best move in every position with real-time engine analysis.

Download Free Extension

You might also like

12 min read

The Immortal Game: Anderssen vs Kieseritzky Explained

Explore the Immortal Game of chess, Anderssen vs Kieseritzky (1851), with interactive positions, PGN, famous sacrifices, and tactical lessons.

  • history
  • tactics
  • famous-games
  • kings-gambit
  • analysis