Skip to content

Personal live dashboard: production hardening - #1

Merged
shashank03-dev merged 5 commits into
mainfrom
feat/web-live-dashboard
Jul 13, 2026
Merged

Personal live dashboard: production hardening#1
shashank03-dev merged 5 commits into
mainfrom
feat/web-live-dashboard

Conversation

@shashank03-dev

Copy link
Copy Markdown
Owner

Reviews and hardens the live-dashboard branch for production.

What was wrong

  • /particle-demo was a public production route. A temp harness marked "delete when done" was compiling into the build output. Removed.
  • A Neon cold start 500s the first request. Neon auto-suspends when idle and the first connection after a suspend fails while the compute wakes (1–5s), with nothing retrying it.
  • Browsers without WebGL got a console error and a black page. OGL logs unable to create webgl context and throws on a null context. GPU-less VMs, blocklisted drivers, and hardware acceleration switched off all hit this.
  • The GL context leaked on every rebuild. OGL never releases it (TODO: Handle context loss in Renderer.js), so past Chrome's ~16-context cap the field silently dies for the rest of the session.
  • A tier change mid-birth stranded the field invisible. The rebuild swapped in fresh uniform objects that no effect ever tweened.
  • Backing out of Discord left a dead sign-in button. bfcache restores the DOM with the pending state intact, and its CSS sets pointer-events: none.
  • Turbopack inferred the wrong workspace root. Competing lockfiles keyed client modules under a root the React Client Manifest didn't share, 500ing renders at random.

Notable decisions

  • Retry is applied to connection acquisition only, never to a statement. A connection that drops mid-query is not retried: the server may already have applied the write, so re-sending it would double-insert. Acquisition is the only step that is safely idempotent. Bounded to ~13s — if Neon is genuinely down, failing fast beats pinning a serverless function open while every queued visitor stacks up behind it. lib/db/index.test.ts guards that budget.
  • No-WebGL users get a still tier-colored wash, not the black void a signed-out visitor sees. They're signed in and have earned a tier; the fallback keeps the ambiance and drops the motion.

Before merging

BASE_URL must be set in Vercel's production environment. lib/env.ts falls back to http://localhost:3000 when it's unset, and the Discord OAuth state cookie is host-scoped — written on one host, read on another, the callback correctly 400s.

Verification

npm run lint, npm run typecheck, npm test (50/50), and npm run build all pass. The no-WebGL fallback was confirmed rendering in a real signed-in browser session that cannot create a WebGL context.

🤖 Generated with Claude Code

shashank03-dev and others added 3 commits July 12, 2026 16:18
Server-synced personal card in a top-right dropdown over the leaderboard,
with a whole-page tier-tinted interactive particle background and an
accurate online signal (new sync `active` flag + lastSeenAt fallback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-in users get a dropdown dashboard over the leaderboard: a frosted-glass
card showing tier, active hours, combos, XP, next-tier progress, per-tool
breakdown, plan, rank against the whole field, and a live online state.

A whole-page particle background (OGL/WebGL) is black when signed out and takes
birth in a staggered swirl once authenticated. Particle color maps to the user's
tier, shifts hue with the cursor, and energizes toward phosphor green while a
tool is actively running. GSAP drives the birth, energy and hue as shader
uniforms so nothing re-renders per frame. Respects prefers-reduced-motion and
prefers-reduced-transparency, degrades to the plain black background when WebGL
is unavailable, and never intercepts board clicks or scroll.

Presence is a new accurate signal: the agent reports an `active` flag plus the
tool ids running now (agent 0.3.0); the server stamps last_active_at only on
active syncs, so freshness alone means "coding now", with the existing heartbeat
as fallback for older agents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…opack root

Fixes found while reviewing the live-dashboard branch for shipping:

- db: retry connection ACQUISITION only (never a statement) so a Neon cold
  start doesn't 500 the first request, while a mid-query drop is never
  re-sent and cannot double-write. Bounded to ~13s, with a connect timeout so
  a black-holed address fails fast instead of hanging on the OS TCP timeout.
  An idle-connection 'error' listener keeps a pooled death from taking down
  the whole serverless instance. Regression test guards the retry budget.

- particles: a browser with no WebGL (GPU-less VM, blocklisted driver,
  hardware acceleration off) made OGL log an error and throw on every mount.
  Probe first via lib/webgl, and give signed-in users a still tier-colored
  wash instead of the black void a signed-out visitor sees. Release the GL
  context on teardown — OGL never does, and past Chrome's ~16-context cap the
  field silently dies for the rest of the session.

- particles: preserve reveal progress across a rebuild and re-arm the tween
  against the new uniforms, so a tier change mid-birth resumes instead of
  stranding the field invisible.

- user-menu: reset the pending state on bfcache restore, so backing out of
  Discord doesn't leave a dead pointer-events:none spinner.

- next.config: pin turbopack.root to this file's own directory. Competing
  lockfiles made root inference key client modules under a root the React
  Client Manifest didn't share, 500ing renders at random.

- remove the temp /particle-demo harness, which was compiling into a public
  production route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
grindboard Ready Ready Preview, Comment Jul 13, 2026 3:57am

…xt back

A WebGL context is a loan. The browser reclaims it on a GPU reset, a driver
swap, a wake from sleep, or when too many other tabs want one — and the canvas
goes dead and stays dead. We ignored `webglcontextlost`, so any of those cost
the visitor the field for the rest of their session.

- Prevent the default on `webglcontextlost` (an unprevented loss is final) and
  rebuild the scene: restoration hands back a blank context, so every program,
  buffer, and attribute OGL uploaded has to be re-uploaded anyway. Reveal
  progress already survives a rebuild, so the field picks up where it left off.
- Retry a failed rebuild on a widening delay, bounded, since the GPU may still
  be coming back from the very reset that triggered it. A first-build failure
  is not retried: that browser simply has no WebGL, and there is nothing to
  wait for.
- Detach the loss handler before the deliberate loseContext() in teardown, or
  unmounting trips our own recovery and every unmount spawns a replacement.
- Probe `experimental-webgl` too, so browsers that only answer to the old name
  get the field instead of the fallback background.

Verified against a real context loss in headless Chrome with a live SwiftShader
context: after loseContext(), the component mounts a new canvas with a live
context (1280x657), exactly one canvas, no rebuild loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Production had BASE_URL set to an empty string. `??` only guards null and
undefined, so "" sailed through as a legitimate value: baseUrl became "", the
Discord redirect_uri degraded to a bare "/auth/callback", and sign-in broke on
grindeasy.tech while every other page looked healthy. Had the variable simply
been absent, the VERCEL_PROJECT_PRODUCTION_URL fallback on the next line would
have produced the right answer all along.

Blank now means absent, so the fallback does its job. Per Vercel's docs
VERCEL_PROJECT_PRODUCTION_URL is the project's shortest production custom
domain (grindeasy.tech), set on every deployment, protocol not included.

Tests cover the set, blank, whitespace-only, trailing-slash, and unset cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shashank03-dev
shashank03-dev merged commit 167680d into main Jul 13, 2026
3 of 4 checks passed
@shashank03-dev
shashank03-dev deleted the feat/web-live-dashboard branch July 13, 2026 03:56
shashank03-dev added a commit that referenced this pull request Jul 16, 2026
Server-synced personal card in a top-right dropdown over the leaderboard,
with a whole-page tier-tinted interactive particle background and an
accurate online signal (new sync `active` flag + lastSeenAt fallback).

* feat: personal live dashboard with tier-colored particle background

Signed-in users get a dropdown dashboard over the leaderboard: a frosted-glass
card showing tier, active hours, combos, XP, next-tier progress, per-tool
breakdown, plan, rank against the whole field, and a live online state.

A whole-page particle background (OGL/WebGL) is black when signed out and takes
birth in a staggered swirl once authenticated. Particle color maps to the user's
tier, shifts hue with the cursor, and energizes toward phosphor green while a
tool is actively running. GSAP drives the birth, energy and hue as shader
uniforms so nothing re-renders per frame. Respects prefers-reduced-motion and
prefers-reduced-transparency, degrades to the plain black background when WebGL
is unavailable, and never intercepts board clicks or scroll.

Presence is a new accurate signal: the agent reports an `active` flag plus the
tool ids running now (agent 0.3.0); the server stamps last_active_at only on
active syncs, so freshness alone means "coding now", with the existing heartbeat
as fallback for older agents.

* fix: harden dashboard for production — db retry, WebGL fallback, Turbopack root

Fixes found while reviewing the live-dashboard branch for shipping:

- db: retry connection ACQUISITION only (never a statement) so a Neon cold
  start doesn't 500 the first request, while a mid-query drop is never
  re-sent and cannot double-write. Bounded to ~13s, with a connect timeout so
  a black-holed address fails fast instead of hanging on the OS TCP timeout.
  An idle-connection 'error' listener keeps a pooled death from taking down
  the whole serverless instance. Regression test guards the retry budget.

- particles: a browser with no WebGL (GPU-less VM, blocklisted driver,
  hardware acceleration off) made OGL log an error and throw on every mount.
  Probe first via lib/webgl, and give signed-in users a still tier-colored
  wash instead of the black void a signed-out visitor sees. Release the GL
  context on teardown — OGL never does, and past Chrome's ~16-context cap the
  field silently dies for the rest of the session.

- particles: preserve reveal progress across a rebuild and re-arm the tween
  against the new uniforms, so a tier change mid-birth resumes instead of
  stranding the field invisible.

- user-menu: reset the pending state on bfcache restore, so backing out of
  Discord doesn't leave a dead pointer-events:none spinner.

- next.config: pin turbopack.root to this file's own directory. Competing
  lockfiles made root inference key client modules under a root the React
  Client Manifest didn't share, 500ing renders at random.

- remove the temp /particle-demo harness, which was compiling into a public
  production route.

* fix(particles): recover the field when the browser takes the GL context back

A WebGL context is a loan. The browser reclaims it on a GPU reset, a driver
swap, a wake from sleep, or when too many other tabs want one — and the canvas
goes dead and stays dead. We ignored `webglcontextlost`, so any of those cost
the visitor the field for the rest of their session.

- Prevent the default on `webglcontextlost` (an unprevented loss is final) and
  rebuild the scene: restoration hands back a blank context, so every program,
  buffer, and attribute OGL uploaded has to be re-uploaded anyway. Reveal
  progress already survives a rebuild, so the field picks up where it left off.
- Retry a failed rebuild on a widening delay, bounded, since the GPU may still
  be coming back from the very reset that triggered it. A first-build failure
  is not retried: that browser simply has no WebGL, and there is nothing to
  wait for.
- Detach the loss handler before the deliberate loseContext() in teardown, or
  unmounting trips our own recovery and every unmount spawns a replacement.
- Probe `experimental-webgl` too, so browsers that only answer to the old name
  get the field instead of the fallback background.

Verified against a real context loss in headless Chrome with a live SwiftShader
context: after loseContext(), the component mounts a new canvas with a live
context (1280x657), exactly one canvas, no rebuild loop.

* fix(env): treat a blank BASE_URL as absent, not as a valid base URL

Production had BASE_URL set to an empty string. `??` only guards null and
undefined, so "" sailed through as a legitimate value: baseUrl became "", the
Discord redirect_uri degraded to a bare "/auth/callback", and sign-in broke on
grindeasy.tech while every other page looked healthy. Had the variable simply
been absent, the VERCEL_PROJECT_PRODUCTION_URL fallback on the next line would
have produced the right answer all along.

Blank now means absent, so the fallback does its job. Per Vercel's docs
VERCEL_PROJECT_PRODUCTION_URL is the project's shortest production custom
domain (grindeasy.tech), set on every deployment, protocol not included.

Tests cover the set, blank, whitespace-only, trailing-slash, and unset cases.

---------
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