Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
919dffa
feat(goals): add auto-progress sync from GitHub commit data (#190)
omkhandare55 May 17, 2026
85def43
fix(goals/sync): use period_start range filter and fix Sunday/timezon…
omkhandare55 May 17, 2026
1e1c415
fix(goals/sync): fix auto-sync loop, simplify logic, update migration…
omkhandare55 May 19, 2026
6cd0054
test(goals): fix playwright dashboard widget tests for new ui
omkhandare55 May 19, 2026
0cc3d1e
Resolve merge conflicts and address review comments
omkhandare55 May 20, 2026
c901172
chore: trigger labeler workflow
omkhandare55 May 20, 2026
e9d78c2
Merge branch 'main' into feat/goal-auto-progress-commits
omkhandare55 May 21, 2026
2f0c86e
feat: unify destructive styling and error widgets to use css theme va…
omkhandare55 May 22, 2026
50b4184
test: resolve e2e dashboard widgets spec conflict and adopt robust ti…
omkhandare55 May 22, 2026
ddbc0ef
Merge branch 'main' into feat/goal-auto-progress-commits
omkhandare55 May 22, 2026
ec03216
Merge branch 'main' into feat/goal-auto-progress-commits
omkhandare55 May 22, 2026
c8de929
Merge branch 'main' into feat/goal-auto-progress-commits
omkhandare55 May 22, 2026
9397147
fix: resolve duplicate unit declaration in goals route
omkhandare55 May 22, 2026
21a7c15
Merge upstream/main into feat/goal-auto-progress-commits
omkhandare55 May 22, 2026
40b3cc0
fix: resolve type errors in contributions route, add issues cache key…
omkhandare55 May 22, 2026
2b0cb3e
fix: resolve merge conflicts with upstream/main — keep auto-sync feat…
omkhandare55 May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions e2e/dashboard-widgets.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
await expect(page.getByRole("heading", { name: "Dashboard" })).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: "Weekly Goals" })).toBeVisible({ timeout: 10000 });
await expect(page.getByRole("heading", { name: "Goals" })).toBeVisible({ timeout: 10000 });

Check failure on line 127 in e2e/dashboard-widgets.spec.js

View workflow job for this annotation

GitHub Actions / Playwright smoke tests

[chromium] › dashboard-widgets.spec.js:122:5 › dashboard widgets render with mocked metrics

1) [chromium] › dashboard-widgets.spec.js:122:5 › dashboard widgets render with mocked metrics ─── Error: Timed out 10000ms waiting for expect(locator).toBeVisible() Locator: getByRole('heading', { name: 'Goals' }) Expected: visible Received: <element(s) not found> Call log: - expect.toBeVisible with timeout 10000ms - waiting for getByRole('heading', { name: 'Goals' }) 125 | await expect(page.getByRole("heading", { name: "Your Commits" })).toBeVisible({ timeout: 10000 }); 126 | await expect(page.getByRole("heading", { name: "PR Analytics" })).toBeVisible({ timeout: 10000 }); > 127 | await expect(page.getByRole("heading", { name: "Goals" })).toBeVisible({ timeout: 10000 }); | ^ 128 | await expect(page.getByText("Make 10 commits")).toBeVisible({ timeout: 10000 }); 129 | }); 130 | at /home/runner/work/devtrack/devtrack/e2e/dashboard-widgets.spec.js:127:62
await expect(page.getByText("Make 10 commits")).toBeVisible({ timeout: 10000 });
});

Expand Down Expand Up @@ -155,14 +155,14 @@
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible({ timeout: 30000 });
await page.getByLabel("Goal title").fill("Ship one PR");
await page.getByLabel("Target").fill("1");
await page.getByLabel("Unit").fill("PR");
await page.getByLabel("Unit").selectOption("prs");
await page.getByRole("button", { name: "Add goal" }).click();

await expect.poll(() => goalPosts, { timeout: 15000 }).toHaveLength(1);
expect(goalPosts[0]).toMatchObject({
title: "Ship one PR",
target: 1,
unit: "PR",
unit: "prs",
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/app/api/goals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,4 @@ try {
if (error) return Response.json({ error: error.message }, { status: 500 });

return Response.json({ goal }, { status: 201 });
}
}
109 changes: 109 additions & 0 deletions src/app/api/goals/sync/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";

export const dynamic = "force-dynamic";

/**
* Returns Monday 00:00:00 local time of the current week as a full ISO string.
*
* Fix for Bug 2 (Sunday + timezone):
* - Uses `const diff = day === 0 ? -6 : 1 - day` so Sunday correctly resolves
* to the *previous* Monday, not the *next* one.
* - Returns a full ISO timestamp instead of `.slice(0, 10)` to avoid the UTC
* date-shift bug (matching the approach already used in `getPeriodStart()`).
*/
function currentWeekStart(): string {
const now = new Date();
const day = now.getUTCDay();
const diff = day === 0 ? -6 : 1 - day; // Monday = 0 offset; Sunday = -6
const monday = new Date(now);
monday.setUTCDate(now.getUTCDate() + diff);
monday.setUTCHours(0, 0, 0, 0);
return monday.toISOString();
}

/** Returns Sunday 23:59:59.999 of the current week as a full ISO string. */
function currentWeekEnd(): string {
const now = new Date();
const day = now.getUTCDay();
const diff = day === 0 ? 0 : 7 - day; // Sunday of this week
const sunday = new Date(now);
sunday.setUTCDate(now.getUTCDate() + diff);
sunday.setUTCHours(23, 59, 59, 999);
return sunday.toISOString();
}

const GITHUB_API = "https://api.github.com";

export async function POST() {
const session = await getServerSession(authOptions);
if (!session?.accessToken || !session.githubId || !session.githubLogin) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

// ── 1. Fetch user from DB ─────────────────────────────────────────────────
const { data: user } = await supabaseAdmin
.from("users")
.select("id")
.eq("github_id", session.githubId)
.single();

if (!user) return Response.json({ error: "User not found" }, { status: 404 });

const weekStart = currentWeekStart();
const weekEnd = currentWeekEnd();

// ── 2. Fetch all commit-based goals for this week ─────────────────────────
// Fix for Bug 1: column is `period_start` (full ISO timestamp), not `week_start`.
// Use a range filter (.gte / .lte) instead of string equality.
const { data: commitGoals, error: goalsError } = await supabaseAdmin
.from("goals")
.select("id")
.eq("user_id", user.id)
.eq("unit", "commits")
.gte("period_start", weekStart)
.lte("period_start", weekEnd);

if (goalsError) {
return Response.json({ error: "Failed to fetch goals" }, { status: 500 });
}

if (!commitGoals || commitGoals.length === 0) {
return Response.json({ updated: 0, commitCount: 0 });
}

// ── 3. Count commits for the current week from GitHub ────────────────────
const ghRes = await fetch(
`${GITHUB_API}/search/commits?q=author:${session.githubLogin}+author-date:${weekStart}..${weekEnd}&per_page=100`,
{
headers: {
Authorization: `Bearer ${session.accessToken}`,
Accept: "application/vnd.github+json",
},
cache: "no-store",
}
);

if (!ghRes.ok) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}

const ghData = (await ghRes.json()) as { total_count: number };
const commitCount = ghData.total_count;

// ── 4. Update all commit-based goals with the real commit count ───────────
const now = new Date().toISOString();
const ids = commitGoals.map((g) => g.id);

const { error: updateError } = await supabaseAdmin
.from("goals")
.update({ current: commitCount, last_synced_at: now })
.in("id", ids);

if (updateError) {
return Response.json({ error: "Failed to update goals" }, { status: 500 });
}

return Response.json({ updated: ids.length, commitCount });
}
14 changes: 2 additions & 12 deletions src/app/api/metrics/contributions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,7 @@ async function fetchContributionsForAccount(
since.setDate(since.getDate() - days);
const sinceStr = fromDate ?? toLocalDateStr(since);

let allItems: Array<{
sha: string;
html_url: string;
repository?: { full_name: string };
commit: { author: { date: string }; message: string };
}> = [];
let allItems: GitHubCommitSearchItem[] = [];
const commitItems: CommitItem[] = [];
let totalCount = 0;
let page = 1;
Expand Down Expand Up @@ -122,12 +117,7 @@ async function fetchContributionsForAccount(

const data = (await searchRes.json()) as {
total_count: number;
items: Array<{
sha: string;
html_url: string;
repository?: { full_name: string };
commit: { author: { date: string }; message: string };
}>;
items: GitHubCommitSearchItem[];
};

if (page === 1) {
Expand Down
6 changes: 3 additions & 3 deletions src/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ function SettingsPageContent() {
className={`mb-6 rounded-xl border p-4 text-sm ${
statusMessage.kind === "success"
? "border-green-500/30 bg-green-500/10 text-green-400"
: "border-red-500/30 bg-red-500/10 text-red-400"
: "border-[var(--destructive-muted-border)] bg-[var(--destructive-muted)] text-[var(--destructive)]"
}`}
>
{statusMessage.message}
Expand Down Expand Up @@ -492,7 +492,7 @@ function SettingsPageContent() {
</div>

{removeError && (
<div className="mt-4 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
<div className="mt-4 rounded-lg border border-[var(--destructive-muted-border)] bg-[var(--destructive-muted)] p-3 text-sm text-[var(--destructive)]">
{removeError}
</div>
)}
Expand Down Expand Up @@ -532,7 +532,7 @@ function SettingsPageContent() {
onClick={() => handleRemoveAccount(account.githubId)}
aria-label={`Remove ${account.githubLogin}`}
disabled={removingAccountId === account.githubId}
className="rounded-lg border border-[var(--border)] px-4 py-2 text-sm font-medium text-[var(--card-foreground)] transition-colors hover:bg-red-500/10 hover:text-red-400 disabled:opacity-60"
className="rounded-lg border border-[var(--border)] px-4 py-2 text-sm font-medium text-[var(--card-foreground)] transition-colors hover:bg-[var(--destructive-muted)] hover:text-[var(--destructive)] disabled:opacity-60"
>
{removingAccountId === account.githubId
? "Removing..."
Expand Down
8 changes: 8 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
--control-hover: #cbd5e1;
--tooltip: #ffffff;
--tooltip-foreground: #0f172a;
--destructive: #ef4444;
--destructive-muted: rgba(239, 68, 68, 0.1);
--destructive-muted-border: rgba(239, 68, 68, 0.3);
--destructive-foreground: #ffffff;
}

.dark {
Expand All @@ -42,6 +46,10 @@
--control-hover: #475569;
--tooltip: #1e293b;
--tooltip-foreground: #f8fafc;
--destructive: #f87171;
--destructive-muted: rgba(248, 113, 113, 0.1);
--destructive-muted-border: rgba(248, 113, 113, 0.3);
--destructive-foreground: #ffffff;
}

body {
Expand Down
Loading
Loading