From de9360377f4980ffcdc5486be4af92c0d2c5b392 Mon Sep 17 00:00:00 2001 From: Rohith31-WD Date: Mon, 25 May 2026 17:26:15 +0530 Subject: [PATCH 1/4] Implement close functionality for DailyBreakdownSheet Implemented `outside click` and `Escape key` handling for improved sheet dismissal UX and accessibility. --- src/components/DailyBreakdownSheet.tsx | 31 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/components/DailyBreakdownSheet.tsx b/src/components/DailyBreakdownSheet.tsx index 07a4f225c..36561ebb5 100644 --- a/src/components/DailyBreakdownSheet.tsx +++ b/src/components/DailyBreakdownSheet.tsx @@ -45,7 +45,25 @@ export default function DailyBreakdownSheet({ useEffect(() => { onCloseRef.current = onClose; }, [onClose]); +const sheetRef = useRef(null); +useEffect(() => { + if (!isOpen) return; + const handleClickOutside = (e: MouseEvent | TouchEvent) => { + if ( + sheetRef.current && + !sheetRef.current.contains(e.target as Node) + ) { + onClose(); + } + }; + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("touchstart", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("touchstart", handleClickOutside); + }; +}, [isOpen, onClose]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") onCloseRef.current(); @@ -72,12 +90,13 @@ export default function DailyBreakdownSheet({ onClick={onClose} aria-hidden="true" /> -
+

From 8c9d5d2afe1d2ba70196e4a487324dbd377b6aae Mon Sep 17 00:00:00 2001 From: Rohith PM Date: Wed, 27 May 2026 00:36:50 +0530 Subject: [PATCH 2/4] Fix: Move metric routes loop inside beforeEach hook The for loop that was setting up route mocks (lines 176-183) was executing at module load time instead of during test setup. This caused Playwright to try to execute page.route() outside of a test context, triggering the error 'test.beforeEach() to be called here'. Moving the loop inside the beforeEach hook ensures all route setup happens at the correct time during test initialization. --- e2e/dashboard-widgets.spec.js | 49 ++++++----------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/e2e/dashboard-widgets.spec.js b/e2e/dashboard-widgets.spec.js index 6f6fa7457..dd788ee91 100644 --- a/e2e/dashboard-widgets.spec.js +++ b/e2e/dashboard-widgets.spec.js @@ -118,38 +118,6 @@ test.beforeEach(async ({ page }) => { }); }); - await page.route("**/api/ai-insights**", async (route) => { - await route.fulfill({ - contentType: "application/json", - body: JSON.stringify({ - data: { - insights: [ - { - id: "insight-1", - type: "productivity", - title: "High Consistency", - description: "You have coded 5 days this week!", - severity: "positive", - }, - ], - trend: { direction: "up", percentage: 15 }, - aiSummary: "Great job shipping features this week. Keep up the high standard!", - generatedAt: "2026-05-18T12:00:00.000Z", - }, - }), - }); - }); - - await page.route("**/api/notifications**", async (route) => { - await route.fulfill({ - contentType: "application/json", - body: JSON.stringify({ - notifications: [], - unreadCount: 0, - }), - }); - }); - const metricRoutes = [ "**/api/metrics/prs**", "**/api/metrics/pr-breakdown**", @@ -170,19 +138,18 @@ test.beforeEach(async ({ page }) => { "**/api/metrics/discussions**", "**/api/metrics/pr-review-trend**", "**/api/metrics/inactive-repos**", - "**/api/notifications**", ]; -for (const pattern of metricRoutes) { - await page.route(pattern, async (route) => { - await route.fulfill({ - contentType: "application/json", - body: JSON.stringify(mockMetricResponse(route.request().url())), + for (const pattern of metricRoutes) { + await page.route(pattern, async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify(mockMetricResponse(route.request().url())), + }); }); - }); -} - + } }); + test("dashboard widgets render with mocked metrics", async ({ page }) => { await page.goto("/dashboard", { waitUntil: "load" }); await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible({ timeout: 30000 }); From 3be0f188dfcf8049d76a0b0caff018345859c50d Mon Sep 17 00:00:00 2001 From: Rohith31-WD Date: Sun, 31 May 2026 16:41:50 +0530 Subject: [PATCH 3/4] fix goal progress verification --- src/app/api/goals/[id]/route.ts | 59 +++++++++------ test/goals-patch-progress.test.ts | 116 ++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 test/goals-patch-progress.test.ts diff --git a/src/app/api/goals/[id]/route.ts b/src/app/api/goals/[id]/route.ts index 5585e0f78..0f5aee284 100644 --- a/src/app/api/goals/[id]/route.ts +++ b/src/app/api/goals/[id]/route.ts @@ -5,6 +5,8 @@ import { resolveAppUser } from "@/lib/resolve-user"; export const dynamic = "force-dynamic"; +const AUTO_SYNCED_GOAL_UNITS = new Set(["commits", "prs"]); + export async function DELETE( _req: Request, { params }: { params: { id: string } } @@ -97,30 +99,45 @@ export async function PATCH( updates.target = target; } -if (current !== undefined) { - if ( - typeof current !== "number" || - !Number.isInteger(current) || - current < 0 - ) { - return Response.json( - { error: "current must be a non-negative integer" }, - { status: 400 } - ); - } + if (current !== undefined) { + if (AUTO_SYNCED_GOAL_UNITS.has(existing.unit)) { + return Response.json( + { error: "current for GitHub-synced goals can only be updated by sync" }, + { status: 400 } + ); + } - const effectiveTarget = - typeof target === "number" ? target : existing.target; + const requestedUnit = typeof unit === "string" ? unit : existing.unit; + if (AUTO_SYNCED_GOAL_UNITS.has(requestedUnit)) { + return Response.json( + { error: "current for GitHub-synced goals can only be updated by sync" }, + { status: 400 } + ); + } - if (current > effectiveTarget) { - return Response.json( - { error: "current cannot exceed target" }, - { status: 400 } - ); - } + if ( + typeof current !== "number" || + !Number.isInteger(current) || + current < 0 + ) { + return Response.json( + { error: "current must be a non-negative integer" }, + { status: 400 } + ); + } - updates.current = current; -} + const effectiveTarget = + typeof target === "number" ? target : existing.target; + + if (current > effectiveTarget) { + return Response.json( + { error: "current cannot exceed target" }, + { status: 400 } + ); + } + + updates.current = current; + } if (unit !== undefined) { const safeUnit = typeof unit === "string" ? unit.slice(0, 30) : "commits"; diff --git a/test/goals-patch-progress.test.ts b/test/goals-patch-progress.test.ts new file mode 100644 index 000000000..94adccbd6 --- /dev/null +++ b/test/goals-patch-progress.test.ts @@ -0,0 +1,116 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + getServerSession: vi.fn(), + resolveAppUser: vi.fn(), + from: vi.fn(), + update: vi.fn(), + existingGoal: { + id: "goal-1", + user_id: "user-1", + title: "Ship work", + target: 10, + current: 2, + unit: "tasks", + }, +})); + +vi.mock("next-auth", () => ({ + getServerSession: mocks.getServerSession, +})); + +vi.mock("@/lib/auth", () => ({ + authOptions: {}, +})); + +vi.mock("@/lib/resolve-user", () => ({ + resolveAppUser: mocks.resolveAppUser, +})); + +vi.mock("@/lib/supabase", () => ({ + supabaseAdmin: { + from: mocks.from, + }, +})); + +import { PATCH } from "@/app/api/goals/[id]/route"; + +function createSupabaseBuilder() { + const builder = { + select: vi.fn(() => builder), + eq: vi.fn(() => builder), + update: mocks.update.mockImplementation(() => builder), + maybeSingle: vi.fn(async () => ({ data: mocks.existingGoal, error: null })), + single: vi.fn(async () => ({ + data: { ...mocks.existingGoal, current: 4 }, + error: null, + })), + }; + + return builder; +} + +async function patchGoal(body: Record) { + const req = new Request("http://localhost/api/goals/goal-1", { + method: "PATCH", + body: JSON.stringify(body), + }); + + return PATCH(req, { params: { id: "goal-1" } }); +} + +describe("PATCH /api/goals/[id] progress updates", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.getServerSession.mockResolvedValue({ + githubId: "github-1", + githubLogin: "octocat", + }); + mocks.resolveAppUser.mockResolvedValue({ id: "user-1" }); + mocks.from.mockImplementation(() => createSupabaseBuilder()); + mocks.existingGoal = { + id: "goal-1", + user_id: "user-1", + title: "Ship work", + target: 10, + current: 2, + unit: "tasks", + }; + }); + + it("allows manual progress updates for non-GitHub goals", async () => { + const response = await patchGoal({ current: 4 }); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(json.goal.current).toBe(4); + expect(mocks.update).toHaveBeenCalledWith({ current: 4 }); + }); + + it.each(["commits", "prs"])( + "rejects client-supplied progress for %s goals", + async (unit) => { + mocks.existingGoal = { ...mocks.existingGoal, unit }; + + const response = await patchGoal({ current: 9 }); + const json = await response.json(); + + expect(response.status).toBe(400); + expect(json.error).toBe( + "current for GitHub-synced goals can only be updated by sync" + ); + expect(mocks.update).not.toHaveBeenCalled(); + } + ); + + it("rejects setting progress while converting a goal to a GitHub-synced unit", async () => { + const response = await patchGoal({ unit: "commits", current: 9 }); + const json = await response.json(); + + expect(response.status).toBe(400); + expect(json.error).toBe( + "current for GitHub-synced goals can only be updated by sync" + ); + expect(mocks.update).not.toHaveBeenCalled(); + }); +}); From 3799ca218ce375f63779b874f6adf42996c80427 Mon Sep 17 00:00:00 2001 From: Rohith31-WD Date: Sun, 31 May 2026 17:04:00 +0530 Subject: [PATCH 4/4] fix labeler workflow permissions --- .github/workflows/labeler.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 676e9b6ce..35eec881a 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,12 @@ name: PR Labeler on: pull_request_target: + +permissions: + contents: read + pull-requests: write + issues: write + jobs: label: runs-on: ubuntu-latest