Skip to content

feat(content): ✨ live refresh for AdminCP lists and comment threads - #908

Merged
aXenDeveloper merged 1 commit into
canaryfrom
feat/content-live-refresh
Oct 30, 2026
Merged

aXenDeveloper merged 1 commit into
canaryfrom
feat/content-live-refresh

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Content Engine changes now reach open screens over the existing shared
WebSocket as small, typed hints; HTTP stays the source of truth.

  • Server: authorized content/refresh socket with join acknowledgement,
    per-tab memberships, limits, periodic re-authorization and immediate
    re-checks when a thread's root changes. Signals are published from
    emitContentEvent after the commit and cache invalidation.
  • New GET /{publicApi.path}/thread/branches route reusing the thread
    reader, for targeted, permission-aware branch refreshes.
  • Generated AdminCP lists refresh the page on screen (filters, sort, page and
    selection kept), with coalescing and a non-blocking failure notice.
  • useContentComments gains branches and live: branch merges in place,
    "Show new comments" for new top-level comments, drafts kept when a comment
    is removed, structural sharing and scroll anchoring. Blog and Newsroom use it.
  • Docs: Live comments, Live refresh, Comments API and AdminCP updates.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01CnAiFbH6Ps6sXRHUdJUAZk

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T19:01:09.723140Z a23e102 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

React Doctor found 2 new issues in 2 files · 2 warnings · score 84 / 100 (Needs work) · 0 fixed · vs canary

2 warnings

src/content/server/refresh/server.ts

  • ⚠️ L235 await before an early-return guard async-defer-await

src/tanstack/comments/live.ts

  • ⚠️ L717 await inside a loop async-await-in-loop

Reviewed by React Doctor for commit a23e102. See inline comments for fixes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22e7487292

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +46 to +47
const session = await new SessionAdminModel(c).getSession({ extend: false });
if (!session) return { code: "FORBIDDEN", ok: false };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconnect the socket after AdminCP sign-in

When a visitor signs into AdminCP after the shared WebSocket has opened, this authorization reads the upgrade request's original cookies, which do not contain the newly issued admin session. Both admin sign-in actions in tanstack/admin/actions.ts navigate client-side without reconnecting, and WebSocketAuthSync only reconnects when the member user ID changes. Consequently, the new list subscription receives FORBIDDEN and live refresh remains disabled until a reload or unrelated reconnect. Refresh the shared connection when the AdminCP session changes so the join uses the current admin cookie.

Useful? React with 👍 / 👎.

Comment on lines +207 to +209
if (definition.thread.enabled && touchesPublic) {
const location = await threadLocation(c, definition, contentId, row);
if (location) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Notify the old branch when a reply changes placement

When an admin moves a published reply to another parent or root, this resolves only its post-update location and signals the destination branch. Such updates are allowed by assertContentThreadPlacement (including moving a leaf between roots), but viewers of the original branch receive no signal to remove the reply. Within one thread, refreshing only the destination can leave the same comment rendered under both parents; across roots, the original thread retains the moved comment until a full refetch. Preserve the previous placement and refresh both affected branches or threads.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper force-pushed the feat/content-live-refresh branch from 22e7487 to c523070 Compare October 30, 2026 18:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c523070f8e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const key = shareKey(membership.c, membership.scope);
let pending = key === null ? undefined : shared.get(key);
if (!pending) {
pending = authorize(membership.c, membership.scope).catch(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh permission memos before reauthorizing memberships

When an administrator's content permissions are revoked while their socket remains open, reusing membership.c makes subsequent authorization checks retain the original grants. Both isStaff(..., { live: true }) and checkStaffPermissionOfUser consult the context-keyed memo in api/lib/staff-permission-cache.ts; permission mutations invalidate their own request context and Redis entries, not this retained WebSocket context. Consequently, sweeps continue approving the subscription after HTTP access has been revoked. Use a fresh authorization context or explicitly clear its request-scoped permission memo before each recheck.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines +733 to +737
// Not on screen: "Load more" reads it fresh when it is reached, and at
// the end of the thread the reader decides when to show it.
if (current && current.pages.at(-1)?.pageInfo.hasNextPage === false) {
newBranches.add(key);
setSnapshot({ hasNew: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile newly published branches before the loaded cursor

If an older top-level comment is approved or unhidden after the reader has loaded past its creation time, while more pages remain, its branch is absent from loadedBranches and this condition silently discards its signal. readContentThread paginates by (createdAt, id), so subsequent Load more requests cannot retrieve that newly visible comment before the current cursor. It remains missing until a full refetch or reconnect. Retain these unknown-branch signals and reconcile branches that may have become visible inside the already-loaded range instead of assuming Load more will reach them.

Useful? React with 👍 / 👎.

Comment on lines +375 to +377
items: page.items.flatMap(item =>
gone.has(keyOf(item.id))
? page.items.some(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve open drafts in the failed-refresh removal path

When a removal signal arrives for a comment with an open edit or reply form and the branch request fails, applyRemovals ignores the tracked drafts: a leaf is removed outright, while a parent has its record cleared. Both cases unmount the composer in the shared comment renderer and discard the user's unsaved text. The successful merge path already preserves these nodes through keepDrafts; apply equivalent draft preservation here, marking the retained comment as removed so submission is unavailable without destroying the form.

Useful? React with 👍 / 👎.

Content Engine changes now reach open screens over the existing shared
WebSocket as small, typed hints; HTTP stays the source of truth for content,
visibility and permissions.

- Server: authorized `content/refresh` socket with join acknowledgement,
  per-tab memberships, limits, re-authorization once a minute (including
  sessions that ended while a socket stayed open) and immediate re-checks when
  a thread's root changes. Signals are published from `emitContentEvent` after
  the commit and cache invalidation.
- New `GET /{publicApi.path}/thread/branches` route on the shared thread
  reader, for targeted, permission-aware branch refreshes.
- Generated AdminCP lists refresh the page on screen (filters, sort, page and
  selection kept), with coalescing and a non-blocking failure notice.
- `useContentComments` gains `branches` and `live`: in-place branch merges,
  "Show new comments" for new top-level comments, drafts kept when a comment
  is removed, structural sharing and scroll anchoring. Blog and Newsroom use
  it; the blog thread no longer remounts an open reply form on a first reply.
- `useContentLiveRefresh` for custom screens.
- Tests for the server socket, authorization, publishing order and routing,
  multi-instance delivery, the branches route, the live thread controller
  (with a request-cost benchmark) and the subscription hook.
- Docs: Live comments, Live refresh, Comments API and AdminCP updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CnAiFbH6Ps6sXRHUdJUAZk
@aXenDeveloper
aXenDeveloper force-pushed the feat/content-live-refresh branch from 1d54efe to a23e102 Compare October 30, 2026 18:58
return;
}

const result = await authorize(c, message.scope);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-defer-await (warning)

This await blocks the function before an early-return that doesn't use the awaited value, so the skip path waits for nothing. Move the await below the guard so it only runs when you need the data

Fix → Move the await below the early-return guard so the skip path stays fast and avoids unnecessary async work.

Docs

start += CONTENT_THREAD_BRANCHES_MAX
) {
if (disposed) return;
await request(keys.slice(start, start + CONTENT_THREAD_BRANCHES_MAX));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for-loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

@aXenDeveloper
aXenDeveloper merged commit 58138ed into canary Oct 30, 2026
5 checks passed
@aXenDeveloper
aXenDeveloper deleted the feat/content-live-refresh branch October 30, 2026 18:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a23e102b4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

): null | string => {
if (scope.kind === "admin") return null;

return `${contentRefreshRoom(scope)}|${c.get("user")?.id ?? "guest"}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep session validity checks specific to each connection

When two devices belonging to the same user follow the same thread, this key groups their authorization results even though authorizeContentRefresh checks each connection's session through SessionModel.getUser(). If one device's session is revoked or expires, recheck uses whichever connection is encountered first for both: a valid session can keep the revoked socket subscribed to a hidden root's signals, or an invalid session can revoke the other device's valid subscription. Validate sessions separately before sharing the root-access lookup, or include session identity in the grouping key.

Useful? React with 👍 / 👎.

Comment on lines +539 to +543
await queryClient.refetchQueries({
exact: true,
queryKey: threadKey,
type: "active",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve tracked drafts during full-thread reconciliation

If a reader keeps an edit or reply form open through a disconnect and that comment is deleted or hidden meanwhile, the successful reconnect reconciliation replaces the infinite-query pages directly without applying keepDrafts. The comment consequently disappears or gets record: null, causing ContentCommentItem to unmount the composer and discard its unsaved text. The branch merge preserves tracked drafts, but this full-refetch path needs equivalent preservation when the thread itself remains accessible.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants