Skip to content

Add admin debug trace and fix the grid/dialog jank it found - #517

Closed
fredrivett wants to merge 15 commits into
mainfrom
fredrivett/admin-ux-jank-devtools
Closed

fredrivett wants to merge 15 commits into
mainfrom
fredrivett/admin-ux-jank-devtools

Conversation

@fredrivett

@fredrivett fredrivett commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

What & why

UX jank (the grid jumping as pages load, the item dialog mounting → closing → remounting) was hard to diagnose because each case is a chain across URL writes, React Query refetches, re-renders, and masonry reflows. This adds an admin-only debug trace — a floating panel (enable with ?debug=1 or the admin account menu; anyone in local dev) recording URL writes with call stacks, query fetches/invalidations, masonry reflows (visible moved cards flashed blue), layout shifts, slow frames, and grid/dialog lifecycle + input changes, exportable as JSON (or window.__abodeDebugTrace for automation) — then uses it to find and fix the reported jank:

  • Grid jumping on scroll: the balanced masonry balances a partial last row among its own frames, so when the next page (or its skeletons) filled it, it was rebalanced across all columns and on-screen cards jumped ~2 columns. The grid now renders only complete rows while more pages are coming.
  • Dialog mount → close → remount: useSearch re-parsed the URL on any change, so the dialog's ?item= re-ran the active search and reset its pagination; an item opened from a later page dropped out of the list, the dialog unmounted, fetched it by id and remounted (and closing reset the results again). It now only syncs on search-owned params; debounced writes keep params they don't own.
  • "Loading" flash inside the dialog: lazy detail views suspended on first render even when already loaded, and React holds a committed fallback ~300ms. A small preloadableView renders loaded views without Suspense, and the grid preloads a card's view on pointerover.
  • Opening a note autosaved it: TipTap's setEditable emits an update by default, so every open PATCHed the unchanged note and patched the items cache on close.

Also fixes client state outliving a session: sign-out/sign-in are soft navigations, so the user store (incl. isAdmin), the root React Query cache, and the trace survived into the next user's session in the same tab. The user store is now keyed by user id and cleared on sign-out, and SessionStateReset drops cached queries + the trace whenever the signed-in user goes away or changes.

Checklist

  • User-facing change → added/updated a PostHog event (and a funnel/insight for a new flow), or N/A — N/A: admin tooling + jank fixes to existing flows (no new flow)
  • New error paths report via captureServerException / the error boundary, or N/A — N/A
  • New behavior is covered by tests, or N/A
  • If this fixes a recurring defect, considered a guardrail via the learn skill, or N/A — N/A (each fix has a regression test)

Notes

  • Each fix was reproduced in the browser with the trace first, then verified: zero existing cards move across page loads (desktop + mobile width), a filtered-view item opens with a single mount and no re-search, a hovered card's dialog paints its real view in the first frame, and opening a note sends no PATCH.
  • Follow-ups (not in this PR): deep-linking straight into a dialog logs an aria-hidden hydration mismatch on the command palette (Radix hideOthers runs before the Suspense-wrapped palette hydrates); room pages use the same masonry with a "Load more" button and could adopt the complete-rows hold-back; react-scan / React Query Devtools behind the debug flag.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
abode Ignored Ignored Preview Sep 26, 2026 11:41am UTC

Request Review

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 36 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread app/src/app/layout.tsx
Comment thread app/src/app/(app)/dashboard/items-grid.tsx Outdated
Comment thread app/src/components/debug/debug-session.tsx
Comment thread app/src/lib/debug/trace.ts Outdated
Comment thread app/src/lib/debug/trace.ts Outdated
Comment thread app/src/lib/debug/instrument-performance.ts
Comment thread app/src/components/layout/dashboard-header/client.tsx
Comment thread app/src/app/(app)/dashboard/item-dialog-context.tsx
Comment thread app/src/lib/debug/use-debug-lifecycle.test.tsx
Comment thread app/src/lib/debug/use-debug-lifecycle.ts

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 12 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread app/src/lib/debug/use-debug-lifecycle.test.tsx Outdated
fredrivett and others added 7 commits September 26, 2026 11:56
Bounded in-memory event timeline (no-op unless tracing) plus instrumenters
for URL writes, React Query cache events, layout shifts, slow frames,
masonry reflows, and component lifecycle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Gated to admins (anyone in local dev) behind a per-browser flag, toggled
via ?debug=1 or the admin account menu; the session is code-split.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Capture cold loads opened with ?debug=1, and strip the param once applied
  so it can't override a later menu toggle
- Log :present when tracing starts after a component mounted; skip lifecycle
  diffing while tracing is off
- Pause also stops highlights and grid reflow reporting; a new session
  starts unpaused
- Trim the buffer after inserting so an over-old backdated event is dropped
- Distinguish the grid's first render from an empty list; include the item
  id on closeItem

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign-out/sign-in are soft navigations, so the user store (incl. isAdmin),
the root query cache, and the debug trace outlived the session. Key the
user store by user id (a different user replaces the whole profile),
clear it on sign-out, and drop cached queries + the trace whenever the
signed-in user goes away or changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…test

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@fredrivett
fredrivett force-pushed the fredrivett/admin-ux-jank-devtools branch from 44e925f to afdbf07 Compare September 26, 2026 10:58
@fredrivett fredrivett changed the title Add admin debug trace for investigating UX jank Add admin debug trace; clear user-scoped client state on sign-out Sep 26, 2026
fredrivett and others added 6 commits September 26, 2026 12:05
The balanced masonry assigns each row's frames to columns relative to the
row above, so a partial last row is balanced among just its own frames.
Appending the next page (or its loading skeletons) filled that row and
rebalanced it across every column, jumping cards at the bottom of the
screen sideways right as the load triggered. Render only complete rows
while more pages are coming; the remainder lands with the next page.

Found with the debug trace: a bottom-row card moved ~2 columns on most
page loads; after this, no existing card moves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lets automation/agents read the trace without the clipboard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
useSearch re-parsed the URL on every query-string change, including the
item dialog's ?item=, producing a fresh state (new filter ids) that re-ran
the search and reset its pagination. Opening an item from a later page of
a filtered view dropped it from the results, so the dialog lost its item,
unmounted, fetched it by id and remounted; closing reset the results again
and lost the scroll depth. Only sync on changes to the search-owned params.

Debounced writes now also keep params they don't own, so a typed query
landing after an item opens can't drop ?item= and close the dialog;
immediate writes (a chip click) still replace the whole query.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TipTap's setEditable emits an update by default, and the editability sync
effect runs on mount, so every note open fired onChange: a PATCH of the
unchanged content plus an items-cache patch on close (re-rendering every
card). Pass emitUpdate: false.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Detail views were next/dynamic (React.lazy) chunks, and a lazy component
suspends on its first render even when its chunk is already loaded; React
then holds the committed fallback for its ~300ms throttle. So the first
open of each item kind flashed a Loading placeholder before the real view.

Add preloadableView: once loaded it renders directly with no Suspense
boundary (each instance keeps the path it mounted with, so nothing swaps
mid-life), and expose preload(). The grid preloads a card's detail view on
pointerover (fires just before a tap on touch), so opening renders the
real view in the first frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@fredrivett fredrivett changed the title Add admin debug trace; clear user-scoped client state on sign-out Add admin debug trace and fix the grid/dialog jank it found Sep 26, 2026

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 14 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread app/src/components/note/note-editor.test.tsx Outdated
Comment thread app/src/lib/preloadable-view.tsx Outdated
React.lazy caches its promise for good, including a rejection, so a
transient chunk failure broke that detail view until reload. Recreate the
lazy wrapper after a failed load so a later render retries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread app/src/lib/preloadable-view.test.tsx Outdated
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@fredrivett

Copy link
Copy Markdown
Owner Author

Superseded by the stacked PRs: the debug trace and the elegant fixes landed via #518–#521 and #523, and the grid jank is fixed properly by the masonry stack (#525–#529). The remaining experimental commits are being evaluated separately.

@fredrivett fredrivett closed this Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant