diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0a50912af..4184b4027 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -16,8 +16,8 @@ jobs: runs-on: ubuntu-latest env: NEXTAUTH_SECRET: test-nextauth-secret-for-playwright-tests - NEXTAUTH_URL: http://127.0.0.1:3000 - NEXT_PUBLIC_APP_URL: http://127.0.0.1:3000 + NEXTAUTH_URL: http://127.0.0.1:3002 + NEXT_PUBLIC_APP_URL: http://127.0.0.1:3002 GITHUB_ID: playwright-github-id GITHUB_SECRET: playwright-github-secret NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 676e9b6ce..0752b041a 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -3,8 +3,16 @@ on: pull_request_target: jobs: label: + if: github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: read + pull-requests: write + issues: write runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - uses: actions/labeler@v5 + continue-on-error: true with: repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/labeler.yml diff --git a/e2e/dashboard-widgets.spec.js b/e2e/dashboard-widgets.spec.js index f3e3efb66..62102b912 100644 --- a/e2e/dashboard-widgets.spec.js +++ b/e2e/dashboard-widgets.spec.js @@ -179,7 +179,7 @@ test("dashboard widgets render with mocked metrics", async ({ page }) => { await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible({ timeout: 30000 }); await expect(page.getByRole("heading", { name: "Your Commits" })).toBeVisible({ timeout: 10000 }); await expect(page.getByRole("heading", { name: "PR Analytics" })).toBeVisible({ timeout: 10000 }); - await expect(page.getByRole("heading", { name: "Goals" })).toBeVisible({ timeout: 10000 }); + await expect(page.getByRole("heading", { name: "Goals", exact: true })).toBeVisible({ timeout: 10000 }); await expect(page.getByText("Make 10 commits")).toBeVisible({ timeout: 10000 }); }); @@ -193,6 +193,7 @@ test("contribution graph range buttons request a new range", async ({ page }) => await page.goto("/dashboard", { waitUntil: "load" }); await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible({ timeout: 30000 }); + await page.getByRole("button", { name: "Show 90-day range" }).first().click(); await page .locator("#contribution-activity") .getByRole("button", { name: "Show 90-day range" }) diff --git a/e2e/theme.spec.js b/e2e/theme.spec.js index 39b427369..017e7df36 100644 --- a/e2e/theme.spec.js +++ b/e2e/theme.spec.js @@ -27,6 +27,7 @@ test.beforeEach(async ({ page }) => { httpOnly: true, sameSite: "Lax", secure: false, + expires: Math.floor(Date.now() / 1000) + 60 * 60, }, ]); @@ -59,7 +60,7 @@ test("theme toggle switches between dark and light mode", async ({ page }) => { const initialPressed = await themeToggle.getAttribute("aria-pressed"); - await themeToggle.click(); + await themeToggle.click({ force: true }); await expect(themeToggle).toHaveAttribute( "aria-pressed", diff --git a/package.json b/package.json index 369cc7ec9..0866bde92 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build && node scripts/copy-standalone-static.js", "start": "next start", "lint": "next lint", "type-check": "tsc --noEmit", diff --git a/playwright.config.mjs b/playwright.config.mjs index d33706f91..68224f364 100644 --- a/playwright.config.mjs +++ b/playwright.config.mjs @@ -1,6 +1,6 @@ import { defineConfig, devices } from "@playwright/test"; -const PORT = Number(process.env.PORT ?? 3000); +const PORT = Number(process.env.PORT ?? 3002); const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${PORT}`; const prepareStandaloneCommand = "node -e \"const fs=require('fs'); fs.cpSync('public','.next/standalone/public',{recursive:true,force:true}); fs.cpSync('.next/static','.next/standalone/.next/static',{recursive:true,force:true});\""; diff --git a/scripts/copy-standalone-static.js b/scripts/copy-standalone-static.js new file mode 100644 index 000000000..ef762e26e --- /dev/null +++ b/scripts/copy-standalone-static.js @@ -0,0 +1,34 @@ +const fs = require('fs'); +const path = require('path'); + +function copyDir(src, dest) { + if (!fs.existsSync(src)) return; + fs.mkdirSync(dest, { recursive: true }); + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyDir(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +const standaloneDir = path.join(__dirname, '..', '.next', 'standalone'); + +if (fs.existsSync(standaloneDir)) { + console.log('Copying static files to standalone directory...'); + copyDir( + path.join(__dirname, '..', 'public'), + path.join(standaloneDir, 'public') + ); + copyDir( + path.join(__dirname, '..', '.next', 'static'), + path.join(standaloneDir, '.next', 'static') + ); + console.log('Done.'); +} diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts index d5d1e8167..e65a05ffc 100644 --- a/src/app/api/contact/route.ts +++ b/src/app/api/contact/route.ts @@ -24,7 +24,7 @@ export async function POST(request: NextRequest) { try { payload = (await request.json()) as ContactPayload; - } catch { + } catch (e) { return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); } diff --git a/src/app/api/goals/[id]/route.ts b/src/app/api/goals/[id]/route.ts index 57de979d2..f98ad77db 100644 --- a/src/app/api/goals/[id]/route.ts +++ b/src/app/api/goals/[id]/route.ts @@ -18,15 +18,6 @@ export async function PATCH( const user = await resolveAppUser(session.githubId, session.githubLogin); if (!user) return Response.json({ error: "User not found" }, { status: 404 }); - const body = await req.json().catch(() => ({})); - const { current } = body; - - if (typeof current !== "number" || current < 0) { - return Response.json( - { error: "Invalid current value" }, - { status: 400 } - ); - } const { data: existingGoal } = await supabaseAdmin .from("goals") @@ -39,10 +30,82 @@ export async function PATCH( return Response.json({ error: "Goal not found" }, { status: 404 }); } + let body: unknown; + try { + body = await req.json(); + } catch (e) { + return Response.json({ error: "Invalid JSON" }, { status: 400 }); + } + + if (typeof body !== "object" || body === null) { + return Response.json({ error: "Invalid request body" }, { status: 400 }); + } + + const updates: Record = {}; + + const { title, target, unit, recurrence, current } = + body as Record; + + if (title !== undefined) { + if (typeof title !== "string" || title.trim().length === 0) { + return Response.json({ error: "title must be a non-empty string" }, { status: 400 }); + } + if (title.length > 100) { + return Response.json({ error: "title must be 100 characters or fewer" }, { status: 400 }); + } + updates.title = title.trim(); + } + + if (target !== undefined) { + if ( + typeof target !== "number" || + !Number.isInteger(target) || + target < 1 || + target > 10_000 + ) { + return Response.json( + { error: "target must be an integer between 1 and 10000" }, + { status: 400 } + ); + } + updates.target = target; + } + + if (unit !== undefined) { + if (typeof unit !== "string" || unit.trim().length === 0) { + return Response.json({ error: "unit must be a non-empty string" }, { status: 400 }); + } + updates.unit = unit.trim(); + } + + if (recurrence !== undefined) { + if (recurrence !== "daily" && recurrence !== "weekly" && recurrence !== "monthly") { + return Response.json( + { error: "recurrence must be 'daily', 'weekly', or 'monthly'" }, + { status: 400 } + ); + } + updates.recurrence = recurrence; + } + + if (current !== undefined) { + if (typeof current !== "number" || current < 0) { + return Response.json( + { error: "Invalid current value" }, + { status: 400 } + ); + } + updates.current = current; + } + + if (Object.keys(updates).length === 0) { + return Response.json({ goal: existingGoal }); + } + const wasCompleted = existingGoal.current >= existingGoal.target; const { data: updatedGoal, error } = await supabaseAdmin .from("goals") - .update({ current }) + .update(updates) .eq("id", params.id) .eq("user_id", user.id) .select() diff --git a/src/app/api/goals/route.ts b/src/app/api/goals/route.ts index 156adf786..436fdf842 100644 --- a/src/app/api/goals/route.ts +++ b/src/app/api/goals/route.ts @@ -111,7 +111,7 @@ export async function POST(req: Request) { try { body = await req.json(); -} catch { +} catch (e) { return Response.json({ error: "Invalid JSON" }, { status: 400 }); } diff --git a/src/app/api/integrations/jira/credentials/route.ts b/src/app/api/integrations/jira/credentials/route.ts index e8ef27d8d..821dbba3c 100644 --- a/src/app/api/integrations/jira/credentials/route.ts +++ b/src/app/api/integrations/jira/credentials/route.ts @@ -74,7 +74,7 @@ export async function POST(req: NextRequest) { let body: JiraCredentialsInput; try { body = await req.json(); - } catch { + } catch (e) { return Response.json({ error: "Invalid JSON" }, { status: 400 }); } diff --git a/src/app/api/integrations/jira/route.ts b/src/app/api/integrations/jira/route.ts index f9c919989..ce88d18e4 100644 --- a/src/app/api/integrations/jira/route.ts +++ b/src/app/api/integrations/jira/route.ts @@ -116,7 +116,7 @@ export async function GET(req: NextRequest) { ); } decryptedToken = decrypted; - } catch { + } catch (e) { return Response.json( { error: "Failed to decrypt credentials" }, { status: 500 } @@ -136,7 +136,7 @@ export async function GET(req: NextRequest) { metrics, recentIssues: issues.slice(0, 10), }); - } catch { + } catch (e) { return Response.json( { error: "Failed to fetch Jira data" }, { status: 502 } diff --git a/src/app/api/leaderboard/route.ts b/src/app/api/leaderboard/route.ts index 24ee45469..d75dcb110 100644 --- a/src/app/api/leaderboard/route.ts +++ b/src/app/api/leaderboard/route.ts @@ -18,7 +18,7 @@ import { } from "@/lib/leaderboard"; import { cacheSet } from "@/lib/metrics-cache"; -export const dynamic = "force-dynamic"; +export const revalidate = 3600; const RATE_LIMIT_REQUESTS = 20; const RATE_LIMIT_WINDOW_MS = 60 * 1000; @@ -120,7 +120,7 @@ export async function GET(req: NextRequest) { await cacheSet(LEADERBOARD_CACHE_KEY, payload, CACHE_STALE_SECONDS); setMemoryCachedLeaderboard(payload); return NextResponse.json(payload); - } catch { + } catch (e) { const cached = await cacheGet(LEADERBOARD_CACHE_KEY); if (cached) { return NextResponse.json(cached, { diff --git a/src/app/api/local-coding/keys/route.ts b/src/app/api/local-coding/keys/route.ts index 13f918ede..23f4a0be5 100644 --- a/src/app/api/local-coding/keys/route.ts +++ b/src/app/api/local-coding/keys/route.ts @@ -43,7 +43,7 @@ export async function POST(req: NextRequest) { let body: { name?: string }; try { body = await req.json(); - } catch { + } catch (e) { return Response.json({ error: "Invalid JSON" }, { status: 400 }); } diff --git a/src/app/api/local-coding/sync/route.ts b/src/app/api/local-coding/sync/route.ts index 39e0f8be7..d4b2310ba 100644 --- a/src/app/api/local-coding/sync/route.ts +++ b/src/app/api/local-coding/sync/route.ts @@ -66,7 +66,7 @@ export async function POST(req: NextRequest) { let body: { sessions?: SessionData[] }; try { body = await req.json(); - } catch { + } catch (e) { return Response.json({ error: "Invalid JSON" }, { status: 400 }); } diff --git a/src/app/api/metrics/activity/route.ts b/src/app/api/metrics/activity/route.ts index 9f225edd8..d96df14fd 100644 --- a/src/app/api/metrics/activity/route.ts +++ b/src/app/api/metrics/activity/route.ts @@ -59,7 +59,7 @@ async function fetchFormattedActivityWithFallback( ): Promise { try { return await fetchFormattedActivity(token); - } catch { + } catch (e) { if (!githubLogin) { throw new Error("GitHub API error"); } diff --git a/src/app/api/metrics/ci/route.ts b/src/app/api/metrics/ci/route.ts index fd49028bb..a65b7e45f 100644 --- a/src/app/api/metrics/ci/route.ts +++ b/src/app/api/metrics/ci/route.ts @@ -44,7 +44,7 @@ export async function GET(req: NextRequest) { }); return Response.json(data); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/coding-activity-insights/route.ts b/src/app/api/metrics/coding-activity-insights/route.ts index e96532295..9dd1c6f61 100644 --- a/src/app/api/metrics/coding-activity-insights/route.ts +++ b/src/app/api/metrics/coding-activity-insights/route.ts @@ -40,7 +40,7 @@ function getRequestedTimeZone(req: NextRequest): string { try { new Intl.DateTimeFormat("en-US", { timeZone: raw }).format(new Date()); return raw; - } catch { + } catch (e) { return "UTC"; } } @@ -158,7 +158,7 @@ export async function GET(req: NextRequest) { ); return Response.json(data); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -241,7 +241,7 @@ export async function GET(req: NextRequest) { }); return Response.json(data); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/contributions/daily/route.ts b/src/app/api/metrics/contributions/daily/route.ts index c5123a6a5..f32f5a5d8 100644 --- a/src/app/api/metrics/contributions/daily/route.ts +++ b/src/app/api/metrics/contributions/daily/route.ts @@ -80,7 +80,7 @@ export async function GET(req: NextRequest) { ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } \ No newline at end of file diff --git a/src/app/api/metrics/contributions/hourly/route.ts b/src/app/api/metrics/contributions/hourly/route.ts index 601e34e3e..f4c75abc1 100644 --- a/src/app/api/metrics/contributions/hourly/route.ts +++ b/src/app/api/metrics/contributions/hourly/route.ts @@ -81,7 +81,7 @@ export async function GET(req: NextRequest) { ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/contributions/route.ts b/src/app/api/metrics/contributions/route.ts index a46f692f5..a4d787c53 100644 --- a/src/app/api/metrics/contributions/route.ts +++ b/src/app/api/metrics/contributions/route.ts @@ -367,7 +367,7 @@ export async function GET(req: NextRequest) { repoParam ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -394,7 +394,7 @@ export async function GET(req: NextRequest) { }); return Response.json(merged); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -481,7 +481,7 @@ export async function GET(req: NextRequest) { }); return Response.json(merged); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -513,7 +513,7 @@ export async function GET(req: NextRequest) { fromDate ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/discussions/route.ts b/src/app/api/metrics/discussions/route.ts index acc0df2fd..01c769285 100644 --- a/src/app/api/metrics/discussions/route.ts +++ b/src/app/api/metrics/discussions/route.ts @@ -127,7 +127,7 @@ export async function GET(req: NextRequest) { userId: session.githubId ?? session.githubLogin ?? "primary", }); return Response.json(formatDiscussionsMetrics(result)); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -185,7 +185,7 @@ export async function GET(req: NextRequest) { userId: accountId === session.githubId ? session.githubId : accountId, }); return Response.json(formatDiscussionsMetrics(result)); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/inactive-repos/route.ts b/src/app/api/metrics/inactive-repos/route.ts index ece96cff3..696f54904 100644 --- a/src/app/api/metrics/inactive-repos/route.ts +++ b/src/app/api/metrics/inactive-repos/route.ts @@ -146,7 +146,7 @@ export async function GET(req: NextRequest) { ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -202,7 +202,7 @@ export async function GET(req: NextRequest) { ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -233,7 +233,7 @@ export async function GET(req: NextRequest) { ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/issues/route.ts b/src/app/api/metrics/issues/route.ts index b2d1fe3d3..17edfd6cc 100644 --- a/src/app/api/metrics/issues/route.ts +++ b/src/app/api/metrics/issues/route.ts @@ -49,7 +49,7 @@ export async function GET(req: NextRequest) { () => fetchIssuesMetrics(token!) ); return Response.json(metrics); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/languages/route.ts b/src/app/api/metrics/languages/route.ts index f9c4c99d0..9a1c94364 100644 --- a/src/app/api/metrics/languages/route.ts +++ b/src/app/api/metrics/languages/route.ts @@ -97,7 +97,7 @@ export async function GET(req: NextRequest) { for (const [lang, bytes] of Object.entries(langs)) { langTotals[lang] = (langTotals[lang] ?? 0) + (bytes as number); } - } catch { } + } catch (e) { } }) ); @@ -110,7 +110,7 @@ export async function GET(req: NextRequest) { return { languages }; }); return Response.json(data); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } -} \ No newline at end of file +} diff --git a/src/app/api/metrics/pinned-repos/route.ts b/src/app/api/metrics/pinned-repos/route.ts index a7de84ef3..7a5be4212 100644 --- a/src/app/api/metrics/pinned-repos/route.ts +++ b/src/app/api/metrics/pinned-repos/route.ts @@ -70,7 +70,7 @@ export async function GET() { ); return Response.json({ pinnedRepos: nodes }); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/pr-breakdown/route.ts b/src/app/api/metrics/pr-breakdown/route.ts index b52e2273e..9a3956207 100644 --- a/src/app/api/metrics/pr-breakdown/route.ts +++ b/src/app/api/metrics/pr-breakdown/route.ts @@ -58,7 +58,7 @@ export async function GET(req: NextRequest) { return { draft, open, merged, closed }; }); return Response.json(data); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/pr-review-time/route.ts b/src/app/api/metrics/pr-review-time/route.ts index 24e64eb92..6b657131b 100644 --- a/src/app/api/metrics/pr-review-time/route.ts +++ b/src/app/api/metrics/pr-review-time/route.ts @@ -245,7 +245,7 @@ export async function GET(req: NextRequest) { }); return Response.json(formatTrendWeeks(result)); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -304,7 +304,7 @@ export async function GET(req: NextRequest) { }); return Response.json(formatTrendWeeks(result)); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/api/metrics/prs/route.ts b/src/app/api/metrics/prs/route.ts index 2b0196f8a..6d52120ba 100644 --- a/src/app/api/metrics/prs/route.ts +++ b/src/app/api/metrics/prs/route.ts @@ -501,7 +501,7 @@ async function getGitLabMetrics( try { return await fetchCachedGitLabMRMetrics(token, cacheContext); - } catch { + } catch (e) { return null; } } @@ -620,7 +620,7 @@ export async function GET(req: NextRequest) { fetchReviewMetrics(session.accessToken).catch(() => null), ]); return Response.json({ ...formatPRMetricsResponse(result, gitlab), reviews }); - } catch { + } catch (e) { // Catches errors from fetchCachedPRMetrics (GitHub Search API failures). // Returns 502 so the client knows the data is unavailable, not just empty. return Response.json({ error: "GitHub API error" }, { status: 502 }); @@ -735,7 +735,7 @@ export async function GET(req: NextRequest) { fetchReviewMetrics(selectedAccount.token).catch(() => null), ]); return Response.json({ ...formatPRMetricsResponse(result, gitlab), reviews }); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } -} \ No newline at end of file +} diff --git a/src/app/api/metrics/repo-health/route.ts b/src/app/api/metrics/repo-health/route.ts index 752409068..d9aef5f72 100644 --- a/src/app/api/metrics/repo-health/route.ts +++ b/src/app/api/metrics/repo-health/route.ts @@ -173,7 +173,7 @@ export async function GET(req: NextRequest) { // should not prevent health scores for the remaining repos from loading. const signals = await fetchSignalsForRepo(session.accessToken!, repo.name, days); scores.push(computeHealthScore(repo.name, signals)); - } catch { + } catch (e) { // Swallow per-repo errors (rate limit, private repo, network blip). // The repo is simply omitted from the scores array rather than failing the request. } @@ -181,9 +181,9 @@ export async function GET(req: NextRequest) { return { repos: scores }; }); return Response.json(data); - } catch { + } catch (e) { // Catches errors from fetchReposForAccount (the initial Search API call). // Returns 502 so the client shows an error state rather than an empty health widget. return Response.json({ error: "GitHub API error" }, { status: 502 }); } -} \ No newline at end of file +} diff --git a/src/app/api/metrics/repos/route.ts b/src/app/api/metrics/repos/route.ts index f34903bb6..fe3b540eb 100644 --- a/src/app/api/metrics/repos/route.ts +++ b/src/app/api/metrics/repos/route.ts @@ -236,7 +236,7 @@ export async function GET(req: NextRequest) { { bypass, userId: session.githubId ?? session.githubLogin } ); return Response.json(result); - } catch { + } catch (e) { // fetchReposForAccount throws on GitHub API errors (rate limit, network failure). // Return 502 so the client shows an error state rather than an empty repos widget. return Response.json({ error: "GitHub API error" }, { status: 502 }); @@ -299,7 +299,7 @@ export async function GET(req: NextRequest) { { bypass, userId: session.githubId } ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } @@ -330,7 +330,7 @@ export async function GET(req: NextRequest) { { bypass, userId: accountId } ); return Response.json(result); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } -} \ No newline at end of file +} diff --git a/src/app/api/metrics/weekly-summary/route.ts b/src/app/api/metrics/weekly-summary/route.ts index 9768fa75b..a7afc5d37 100644 --- a/src/app/api/metrics/weekly-summary/route.ts +++ b/src/app/api/metrics/weekly-summary/route.ts @@ -301,7 +301,7 @@ export async function GET(req: NextRequest) { }; }); return Response.json(data); - } catch { + } catch (e) { // Catches errors thrown by the PR Search call or fetchActiveDates (rate limit, network). // Returns 502 so the client shows an error state rather than stale/empty summary data. return Response.json({ error: "GitHub API error" }, { status: 502 }); diff --git a/src/app/api/notifications/discord-sync/route.ts b/src/app/api/notifications/discord-sync/route.ts index 0f1586e0b..1b6dca5a0 100644 --- a/src/app/api/notifications/discord-sync/route.ts +++ b/src/app/api/notifications/discord-sync/route.ts @@ -51,7 +51,7 @@ export async function GET(req: Request) { if (localHour === 24) localHour = 0; isSunday = weekdayPart === "Sun"; - } catch { + } catch (e) { localHour = now.getUTCHours(); isSunday = now.getUTCDay() === 0; } @@ -79,7 +79,7 @@ export async function GET(req: Request) { const dFmt = new Intl.DateTimeFormat("en-US", { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }); const [{value: mo},,{value: da},,{value: ye}] = dFmt.formatToParts(now); todayStr = `${ye}-${mo}-${da}`; - } catch { + } catch (e) { todayStr = toDateStr(now); } diff --git a/src/app/api/user/settings/route.ts b/src/app/api/user/settings/route.ts index b264fa928..e3ef953cf 100644 --- a/src/app/api/user/settings/route.ts +++ b/src/app/api/user/settings/route.ts @@ -204,7 +204,7 @@ export async function PATCH(req: NextRequest) { let body: { is_public?: boolean; leaderboard_opt_in?: boolean; weekly_digest_opt_in?: boolean; pinned_repos?: string[]; wakatime_api_key?: string; discord_webhook_url?: string | null; timezone?: string; bio?: string }; try { body = await req.json(); - } catch { + } catch (e) { return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); } @@ -309,7 +309,7 @@ export async function PATCH(req: NextRequest) { try { Intl.DateTimeFormat(undefined, { timeZone: timezone }); updates.timezone = timezone; - } catch { + } catch (e) { return NextResponse.json({ error: "Invalid timezone" }, { status: 400 }); } } diff --git a/src/app/api/webhooks/github/route.ts b/src/app/api/webhooks/github/route.ts index 728fe969a..3ed3f50d4 100644 --- a/src/app/api/webhooks/github/route.ts +++ b/src/app/api/webhooks/github/route.ts @@ -107,7 +107,7 @@ export async function POST(req: NextRequest) { let payload: GitHubPushPayload; try { payload = JSON.parse(body) as GitHubPushPayload; - } catch { + } catch (e) { return NextResponse.json({ error: "Invalid JSON payload" }, { status: 400 }); } diff --git a/src/app/api/wrapped/route.ts b/src/app/api/wrapped/route.ts index fbfe85124..400e948ac 100644 --- a/src/app/api/wrapped/route.ts +++ b/src/app/api/wrapped/route.ts @@ -160,7 +160,7 @@ async function fetchTopLanguages(token: string, repos: string[]) { for (const [language, bytes] of Object.entries(languages)) { langTotals[language] = (langTotals[language] ?? 0) + bytes; } - } catch { + } catch (e) { // Language data is nice-to-have for the recap. The rest of the wrapped // experience should still render if one repository cannot be read. } @@ -231,7 +231,7 @@ export async function GET(req: NextRequest) { generatedAt: new Date().toISOString(), partial, }); - } catch { + } catch (e) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/app/compare/[users]/page.tsx b/src/app/compare/[users]/page.tsx index ae854923a..179873aa4 100644 --- a/src/app/compare/[users]/page.tsx +++ b/src/app/compare/[users]/page.tsx @@ -21,7 +21,7 @@ function parseUsers(users: string): [string, string] | null { let decoded: string; try { decoded = decodeURIComponent(users); - } catch { + } catch (e) { return null; } diff --git a/src/app/page.tsx b/src/app/page.tsx index d298bc64f..d6f849612 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -74,7 +74,7 @@ async function fetchRepoStats(): Promise { isSponsor: sponsorSet.has(c.login), })); } - } catch { + } catch (e) { // Supabase not configured locally — skip sponsor enrichment, show contributors as-is } } @@ -87,7 +87,7 @@ async function fetchRepoStats(): Promise { goodFirstIssues: Array.isArray(gfiIssues) ? gfiIssues.length : 0, contributors: mappedContributors, }; - } catch { + } catch (e) { return { stars: 0, forks: 0, diff --git a/src/app/u/[username]/feed.xml/route.ts b/src/app/u/[username]/feed.xml/route.ts index f63202bbf..4def40fb4 100644 --- a/src/app/u/[username]/feed.xml/route.ts +++ b/src/app/u/[username]/feed.xml/route.ts @@ -104,7 +104,7 @@ export async function GET( "Cache-Control": "public, max-age=300", }, }); - } catch { + } catch (e) { return new Response("Internal Server Error", { status: 500 }); } } \ No newline at end of file diff --git a/src/components/AccountToggle.tsx b/src/components/AccountToggle.tsx index 49fde40ae..fb94f6002 100644 --- a/src/components/AccountToggle.tsx +++ b/src/components/AccountToggle.tsx @@ -37,7 +37,7 @@ export default function AccountToggle() { githubLogin: account.githubLogin, })) ); - } catch { + } catch (e) { setLinkedAccounts([]); } } diff --git a/src/components/CodingTimeWidget.tsx b/src/components/CodingTimeWidget.tsx index 1af551569..1b4df5442 100644 --- a/src/components/CodingTimeWidget.tsx +++ b/src/components/CodingTimeWidget.tsx @@ -38,7 +38,7 @@ export default function CodingTimeWidget() { const res = await fetch("/api/wakatime"); const json = await res.json(); setData(json); - } catch { + } catch (e) { setData(null); } finally { setLoading(false); diff --git a/src/components/ContributionGraph.tsx b/src/components/ContributionGraph.tsx index 3fd5db119..da8f3eb63 100644 --- a/src/components/ContributionGraph.tsx +++ b/src/components/ContributionGraph.tsx @@ -145,7 +145,7 @@ export default function ContributionGraph() { localStorage.setItem("devtrack:contribution-range", "30"); setDays(30); } - } catch { + } catch (e) { setDays(30); } } @@ -171,7 +171,7 @@ export default function ContributionGraph() { if (typeof window !== "undefined") { try { localStorage.setItem("devtrack:contribution-range", String(newDays)); - } catch { } + } catch (e) {} } }; diff --git a/src/components/ContributionHeatmap.tsx b/src/components/ContributionHeatmap.tsx index c7e2ec57f..21b8881ef 100644 --- a/src/components/ContributionHeatmap.tsx +++ b/src/components/ContributionHeatmap.tsx @@ -118,7 +118,7 @@ export default function ContributionHeatmap({ } else { localStorage.setItem("devtrack:heatmap-range", String(days)); } - } catch { + } catch (e) { setSelectedDays(days); } } @@ -152,7 +152,7 @@ export default function ContributionHeatmap({ if (typeof window !== "undefined") { try { localStorage.setItem("devtrack:heatmap-range", String(newDays)); - } catch {} + } catch (e) {} } }; diff --git a/src/components/FriendComparison.tsx b/src/components/FriendComparison.tsx index 05cd81c81..d357c5078 100644 --- a/src/components/FriendComparison.tsx +++ b/src/components/FriendComparison.tsx @@ -159,7 +159,7 @@ export default function FriendComparison() { }) ); } - } catch { + } catch (e) { setError("An error occurred"); } finally { setLoading(false); diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index 83920c352..284a5a8b7 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -66,7 +66,7 @@ export default function GoalTracker() { if (errData && errData.error) { msg = errData.error; } - } catch {} + } catch (e) {} if (res.status === 401) { msg = "Unauthorized. Please log in again."; } else if (res.status === 502) { @@ -83,7 +83,7 @@ export default function GoalTracker() { await loadGoals(); setLastUpdated(new Date()); setMinutesAgo(0); - } catch { + } catch (e) { setSyncError("Network error. Failed to sync goals."); } finally { setSyncing(false); @@ -154,7 +154,7 @@ export default function GoalTracker() { } else { await loadGoals().catch(() => { }); } - } catch { + } catch (e) { setCreateError("Failed to create goal. Please try again."); } finally { setCreating(false); @@ -174,7 +174,7 @@ export default function GoalTracker() { setGoals(previousGoals); setDeleteError("Failed to delete goal. Please try again."); } - } catch { + } catch (e) { setGoals(previousGoals); setDeleteError("Failed to delete goal. Please check your connection."); } finally { diff --git a/src/components/LocalCodingTime.tsx b/src/components/LocalCodingTime.tsx index 3b88f0b3c..bead00726 100644 --- a/src/components/LocalCodingTime.tsx +++ b/src/components/LocalCodingTime.tsx @@ -48,7 +48,7 @@ export default function LocalCodingTime() { const json = await res.json(); setData(json); } - } catch { + } catch (e) { setData(null); } finally { setLoading(false); diff --git a/src/components/NotificationBell.tsx b/src/components/NotificationBell.tsx index 13ff73b32..c4a787a0b 100644 --- a/src/components/NotificationBell.tsx +++ b/src/components/NotificationBell.tsx @@ -40,7 +40,7 @@ export default function NotificationBell() { if (typeof window !== "undefined") { localStorage.setItem("devtrack:unread-notification-count", count.toString()); } - } catch { + } catch (e) { setError("Failed to load notifications. Please try again later."); } finally { setLoading(false); diff --git a/src/components/PersonalRecords.tsx b/src/components/PersonalRecords.tsx index 1b2244103..0b05455a6 100644 --- a/src/components/PersonalRecords.tsx +++ b/src/components/PersonalRecords.tsx @@ -158,7 +158,7 @@ export default function PersonalRecords() { setStreak(streakData); setContributions(contribData); setRepos(reposData.repos ?? []); - } catch { + } catch (e) { setError("We couldn't load your personal records right now. Please try again in a moment."); } finally { setLoading(false); diff --git a/src/components/PrivacySettings.tsx b/src/components/PrivacySettings.tsx index 4da39c445..d7ca978c4 100644 --- a/src/components/PrivacySettings.tsx +++ b/src/components/PrivacySettings.tsx @@ -33,7 +33,7 @@ export default function PrivacySettings() { URL.revokeObjectURL(url); setMessage({ kind: "success", text: "Data exported successfully" }); - } catch { + } catch (e) { setMessage({ kind: "error", text: "Failed to export data" }); } finally { setDownloading(false); diff --git a/src/components/ProjectMetrics.tsx b/src/components/ProjectMetrics.tsx index a86f21804..b291ef3b5 100644 --- a/src/components/ProjectMetrics.tsx +++ b/src/components/ProjectMetrics.tsx @@ -103,7 +103,7 @@ export default function ProjectMetrics() { setShowForm(false); setFormData({ jiraDomain: "", email: "", apiToken: "", projectKey: "" }); fetchData(); - } catch { + } catch (e) { setConnectionError("Connection failed"); } finally { setConnecting(false); diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 8aafbc7df..7f9adfd80 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -150,7 +150,7 @@ export default function StreakTracker() { if (stored) { try { setDismissedMilestones(JSON.parse(stored)); - } catch { + } catch (e) { // ignore invalid localStorage data } } diff --git a/src/components/TodayFocusHero.tsx b/src/components/TodayFocusHero.tsx index a24b332c5..131781735 100644 --- a/src/components/TodayFocusHero.tsx +++ b/src/components/TodayFocusHero.tsx @@ -65,7 +65,7 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { setGoal(storedGoal); setInputValue(storedGoal); setIsEditing(storedGoal.length === 0); - } catch { + } catch (e) { setGoal(""); setInputValue(""); setIsEditing(true); @@ -80,7 +80,7 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { try { window.localStorage.setItem(todayKey, trimmedGoal); - } catch {} + } catch (e) {} setGoal(trimmedGoal); setInputValue(trimmedGoal); @@ -92,7 +92,7 @@ export default function TodayFocusHero({ userName }: TodayFocusHeroProps) { try { window.localStorage.removeItem(todayKey); - } catch {} + } catch (e) {} setGoal(""); setInputValue(""); diff --git a/src/hooks/useHeatmapTheme.ts b/src/hooks/useHeatmapTheme.ts index b2351c209..9ec08a119 100644 --- a/src/hooks/useHeatmapTheme.ts +++ b/src/hooks/useHeatmapTheme.ts @@ -145,7 +145,7 @@ export function useHeatmapTheme() { if (typeof window !== "undefined") { try { window.localStorage.setItem(STORAGE_KEY, t); - } catch {} + } catch (e) {} window.dispatchEvent(new CustomEvent("heatmap-theme-changed", { detail: t })); } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 307b6a5f6..5fe2f5e54 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -164,7 +164,7 @@ export const authOptions: NextAuthOptions = { } // Non-401 non-ok responses (rate limit, server error) are intentionally // left without updating accessTokenValidatedAt so the next request retries. - } catch { + } catch (e) { // Network failures during validation are not treated as revocation. // The check will be retried on the next request. } diff --git a/src/lib/coding-activity-insights.ts b/src/lib/coding-activity-insights.ts index c6c160a5e..7c261ef64 100644 --- a/src/lib/coding-activity-insights.ts +++ b/src/lib/coding-activity-insights.ts @@ -105,7 +105,7 @@ export function formatTimeZoneLabel(timeZone: string): string { if (offset) { return normalizeOffsetLabel(offset); } - } catch { + } catch (e) { // Fallback to the raw zone name below. } diff --git a/src/lib/github-accounts.ts b/src/lib/github-accounts.ts index f0e302942..6085d4041 100644 --- a/src/lib/github-accounts.ts +++ b/src/lib/github-accounts.ts @@ -73,7 +73,7 @@ export async function getRateLimitRemaining(token: string): Promise { const remaining = data.resources?.core?.remaining; return typeof remaining === "number" ? remaining : 0; - } catch { + } catch (e) { return 0; } } diff --git a/src/lib/goal-tracker.ts b/src/lib/goal-tracker.ts index c5f14b696..9e5eb753a 100644 --- a/src/lib/goal-tracker.ts +++ b/src/lib/goal-tracker.ts @@ -34,7 +34,7 @@ export async function submitGoalWithRefresh({ headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); - } catch { + } catch (e) { return { created: false, error: "Failed to create goal. Please try again.", @@ -54,7 +54,7 @@ export async function submitGoalWithRefresh({ } else { await loadGoals(); } - } catch { + } catch (e) { return { created: true, error: "Goal created, but refreshing goals failed. Please try refreshing.", diff --git a/src/lib/metrics-cache.ts b/src/lib/metrics-cache.ts index 3b0e916db..011e6ea15 100644 --- a/src/lib/metrics-cache.ts +++ b/src/lib/metrics-cache.ts @@ -152,7 +152,7 @@ export async function cacheGet( setMemoryCacheValue(key, redisValue, ttlSeconds); } return redisValue; - } catch { + } catch (e) { return null; } } @@ -174,7 +174,7 @@ export async function cacheSet( if (redis) { try { await redis.set(key, value, { ex: ttlSeconds }); - } catch { + } catch (e) { // Cache failures must not break dashboard metrics. } } @@ -223,7 +223,7 @@ export async function invalidateUserMetricsCache(userId: string): Promise } cursor = Number(nextCursor); } while (cursor !== 0); - } catch { + } catch (e) { // Invalidation failures must not break the webhook response. } } diff --git a/src/lib/sse.ts b/src/lib/sse.ts index e104b5676..adab55602 100644 --- a/src/lib/sse.ts +++ b/src/lib/sse.ts @@ -11,7 +11,7 @@ export function sendSSEEvent( controller.enqueue( `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` ); - } catch { + } catch (e) { sseConnections.delete(userId); } } diff --git a/test.js b/test.js new file mode 100644 index 000000000..9001e5522 --- /dev/null +++ b/test.js @@ -0,0 +1 @@ +console.log(process.cwd()); \ No newline at end of file diff --git a/test/components/DashboardHeader.test.tsx b/test/components/DashboardHeader.test.tsx index 98767b4cd..d5decfcc9 100644 --- a/test/components/DashboardHeader.test.tsx +++ b/test/components/DashboardHeader.test.tsx @@ -1,5 +1,6 @@ import React from "react"; import "@testing-library/jest-dom"; +// @ts-ignore import { render, screen, waitFor } from "@testing-library/react"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import DashboardHeader from "../../src/components/DashboardHeader";