Skip to content
Merged
11 changes: 11 additions & 0 deletions .claude/napkin.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@
- Dev-only third-party scripts should be opt-in; avoid `beforeInteractive` for non-critical tooling (e.g., `react-grab`)
| 2026-02-13 | self | Assumed `request.json()` in Convex HTTP actions returned typed JSON; TS now treats it as `unknown` | Add runtime type guards (or explicit schema validation) before accessing webhook payload fields |
- Convex `httpAction` webhook handlers are safer with explicit type guards before deriving event keys (`object_type`, `aspect_type`) from `request.json()`
- `apps/web` dev script runs `@react-grab/cursor` before `next dev`; this can create behavior differences vs production. Prefer validating layout bugs with `next build && next start` too.
- Overflow debug scripts that add `window.scrollY` to fixed-position elements can produce misleading `top/bottom` values; filter out `position: fixed` when diagnosing document-flow overflow.
- When user asks to branch with dirty worktree, preserve unrelated local modifications and scope edits to requested files only.
| 2026-02-13 | self | Forgot to quote path containing parentheses (`(marketing)`) so zsh globbing failed | Quote paths with `()`, `[]`, and other glob chars in shell commands |
- If user asks to open a PR, commit only task-relevant files and keep unrelated dirty files unstaged.
| 2026-02-13 | self | Used backticks inside a double-quoted `gh pr create --body` string, triggering shell command substitution and noisy side effects | Use a heredoc/file for PR body or avoid backticks in shell-quoted strings |
## User Preferences
- Always commit `.claude/napkin.md` with related task commits.
- Avoid module-scope `new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!)` in route handlers; lazy-init inside request handlers with env guards to prevent build-time crashes.
- Root layout auth preloads can execute during static prerender (`/_not-found`); guard token preload and fail open when auth env vars are absent at build time.
- Vercel can ignore root `vercel.json` when project Root Directory is `apps/web`; keep an `apps/web/vercel.json` with the Convex deploy build command to avoid accidental `pnpm build` fallback.
8 changes: 7 additions & 1 deletion apps/web/app/api/strava/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { api } from "@repo/backend";

import { getServerAuth } from "@/lib/server-auth";

const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const STATE_COOKIE = "strava_oauth_state";
const DEFAULT_SUCCESS_URL = "/integrations?success=strava_connected";
const DEFAULT_ERROR_URL = "/integrations?error=strava_auth_failed";
Expand Down Expand Up @@ -38,6 +37,13 @@ interface StravaTokenResponse {
}

export async function GET(request: NextRequest) {
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
if (!convexUrl) {
console.error("Missing NEXT_PUBLIC_CONVEX_URL for Strava callback route");
return redirectWithCookieClear(request, DEFAULT_ERROR_URL, true);
}
const convex = new ConvexHttpClient(convexUrl);

const { userId, convexToken } = await getServerAuth();

if (!userId) {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/challenges/[id]/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default async function ChallengeAdminLayout({
];

return (
<div className="flex h-screen flex-col bg-zinc-950 text-zinc-100">
<div className="flex h-dvh flex-col overflow-hidden bg-zinc-950 text-zinc-100">
{/* Dense Header Bar */}
<header className="flex-shrink-0 border-b border-zinc-800 bg-zinc-900">
<div className="flex items-center justify-between px-3 py-2">
Expand Down Expand Up @@ -143,7 +143,7 @@ export default async function ChallengeAdminLayout({
</header>

{/* Main Content - Scrollable */}
<main className="flex-1 overflow-y-auto">
<main className="min-h-0 flex-1 overflow-y-auto">
<div className="p-3">
{children}
</div>
Expand Down
26 changes: 16 additions & 10 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,18 @@ export default async function RootLayout({
const enableDevCursorScripts =
process.env.NODE_ENV === "development" &&
process.env.NEXT_PUBLIC_ENABLE_REACT_GRAB === "1";
const canUseAuth = Boolean(process.env.NEXT_PUBLIC_CONVEX_URL);

// getToken() is fast (cookie read) — safe to await in layout.
// The expensive preloadAuthQuery is deferred into Suspense via HeaderContent.
const token = await getToken();
// getToken() is fast (cookie read). During static prerender (e.g. /_not-found)
// auth env vars may be unavailable, so fail open to avoid build-time crashes.
let token: string | null = null;
if (canUseAuth) {
try {
token = (await getToken()) ?? null;
} catch (error) {
console.error("[layout] failed to preload auth token:", error);
}
}

return (
<ConvexProviderWrapper initialToken={token ?? null}>
Expand All @@ -66,13 +74,11 @@ export default async function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} bg-black text-white antialiased`}
>
<div className="relative min-h-screen">
<div className="pointer-events-none fixed inset-0 overflow-hidden">
<div className="absolute -left-20 -top-40 h-96 w-96 rounded-full bg-indigo-500/20 blur-[120px]" />
<div className="absolute bottom-0 right-0 h-80 w-80 translate-x-1/4 translate-y-1/4 rounded-full bg-fuchsia-500/15 blur-[100px]" />
</div>
<Suspense fallback={<HeaderSkeleton />}>
<HeaderContent />
</Suspense>
{canUseAuth && (
<Suspense fallback={<HeaderSkeleton />}>
<HeaderContent />
</Suspense>
)}
<div className="relative z-10">
{children}
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/components/dashboard/dashboard-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function DashboardLayout({
}, []);

return (
<div className="flex h-screen bg-black text-white">
<div className="flex h-dvh overflow-hidden bg-black text-white">
{/* Left Sidebar - Collapsed (lg) */}
<aside onWheel={forwardScroll} className="hidden w-[72px] flex-shrink-0 flex-col border-r border-zinc-800 lg:flex xl:hidden">
<div className="flex h-full flex-col items-center py-4">
Expand Down Expand Up @@ -130,15 +130,15 @@ export function DashboardLayout({
</aside>

{/* Main Content - Scrollable */}
<main ref={mainRef} className="flex-1 overflow-y-auto overscroll-contain scrollbar-hide pb-20 lg:pb-0">
<main ref={mainRef} className="min-h-0 flex-1 overflow-y-auto overscroll-contain scrollbar-hide pb-20 lg:pb-0">
<PaymentRequiredBanner challengeId={challenge.id} />
<AnnouncementBanner challengeId={challenge.id} />
{children}
</main>

{/* Right Sidebar - Fixed (xl only) */}
{!hideRightSidebar && (
<aside onWheel={forwardScroll} className="hidden w-96 flex-shrink-0 flex-col border-l border-zinc-800 lg:flex">
<aside onWheel={forwardScroll} className="hidden min-h-0 w-96 flex-shrink-0 flex-col border-l border-zinc-800 lg:flex">
<div className="p-4">
<UserSearch challengeId={challenge.id} />
</div>
Expand Down
23 changes: 21 additions & 2 deletions apps/web/lib/server-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,30 @@ type ServerAuthResult = {
// Lazy initialization to avoid build-time errors when env vars aren't available
let _betterAuthUtils: ReturnType<typeof convexBetterAuthNextJs> | null = null;

function resolveConvexSiteUrl(convexUrl: string): string {
if (process.env.NEXT_PUBLIC_CONVEX_SITE_URL) {
return process.env.NEXT_PUBLIC_CONVEX_SITE_URL;
}

// Convex cloud deployments use a corresponding ".convex.site" host for auth.
if (convexUrl.includes(".convex.cloud")) {
return convexUrl.replace(".convex.cloud", ".convex.site");
}

// For self-hosted or already-site URLs, reuse the same host.
return convexUrl;
}

function getBetterAuthUtils() {
if (!_betterAuthUtils) {
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
if (!convexUrl) {
throw new Error("NEXT_PUBLIC_CONVEX_URL is not set.");
}

_betterAuthUtils = convexBetterAuthNextJs({
convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
convexUrl,
convexSiteUrl: resolveConvexSiteUrl(convexUrl),
});
}
return _betterAuthUtils;
Expand Down
6 changes: 6 additions & 0 deletions apps/web/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "cd ../../packages/backend && npx convex deploy --cmd 'cd ../../apps/web && npx next build' --cmd-url-env-var-name NEXT_PUBLIC_CONVEX_URL",
"installCommand": "pnpm install",
"framework": "nextjs"
}
5 changes: 5 additions & 0 deletions tasks/2026-02-13-convex-site-url-prerender-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 2026-02-13 Investigate CONVEX_SITE_URL prerender regression

- [x] Locate source of `CONVEX_SITE_URL is not set` throw
- [x] Identify commit/PR introducing the behavior
- [x] Propose/implement fix
5 changes: 5 additions & 0 deletions tasks/2026-02-13-prod-scrollbar-repro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 2026-02-13 Reproduce production-only page scrollbar

- [x] Confirm root/web production scripts
- [x] Provide exact commands to run a production build locally
- [x] Explain likely reasons scrollbar appears in production but not in dev
5 changes: 5 additions & 0 deletions tasks/2026-02-13-vercel-build-command-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 2026-02-13 Align Vercel build command for Convex preview env injection

- [x] Confirm current root `vercel.json` build command
- [x] Add `apps/web/vercel.json` for `apps/web` root-directory deployments
- [x] Push PR update with deployment config alignment
5 changes: 5 additions & 0 deletions tasks/2026-02-13-vercel-build-convex-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 2026-02-13 Fix Vercel build failure for Strava callback route

- [x] Identify build-time crash source in `/api/strava/callback`
- [x] Prevent module-scope Convex client initialization without env vars
- [x] Validate with typecheck