Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6ee30c5
feat(toprepos): add sortable columns with asc/desc toggle (#226)
mallya-m May 17, 2026
60929a9
Add metrics API rate limiting middleware
May 18, 2026
15e2bc6
Remove grid-cols-4 for medium screens
PraneshJha1038 May 16, 2026
47a7aab
Fix overflowing text at pr analytics
PraneshJha1038 May 17, 2026
f7094db
Introduce helper function getCSSVariable to render tooltip styles
PraneshJha1038 May 17, 2026
5af3cef
feat: add accessible keyboard shortcuts modal
seffhunnn May 17, 2026
aa9cb53
feat: enable RLS on all Supabase tables and document security model
devendra-w May 18, 2026
dc062f7
docs: add RLS security model to SECURITY.md
devendra-w May 18, 2026
eabcc40
feat: add monthly active-day trend comparison
seffhunnn May 17, 2026
0c9273e
added area toggle and its function
indresh404 May 19, 2026
8d348df
feat: weekly digest summary card (closes #99)
sanrishi May 17, 2026
773b7ea
Added colour-blind & default friendly heatmap theme
codedbydollys10 May 17, 2026
f3b2974
feat(issues): add mostActiveRepo, assigned issues, and caching to Iss…
vedika76 May 18, 2026
be9be13
Add Playwright E2E smoke tests
saurabhhhcodes May 18, 2026
ec24da9
fix: keep contribution tooltip visible on touch
saurabhhhcodes May 19, 2026
8516b43
feat: add GitHub push webhook refresh endpoint
saurabhhhcodes May 19, 2026
5e6c6da
feat: add CI analytics dashboard widget
saurabhhhcodes May 19, 2026
0eb695f
fix: align repo health score with days filter and add empty state (#83)
sanrishi May 16, 2026
5f5079d
feat(profile): add downloadable OG stats card (#169)
omkhandare55 May 17, 2026
595f112
feat: handle GitHub API rate limits with user-visible feedback (#93)
Harsh-Codes-77 May 19, 2026
206e20b
feat: include freeze days in streak calendar
Harsh-Codes-77 May 19, 2026
7f2301a
fix: restore missing rate-limit banner and build typings
Harsh-Codes-77 May 19, 2026
634ecae
fix: resolve conflicts in rate limit handling components
Harsh-Codes-77 May 19, 2026
db3ae90
fix: resolve conflicts by taking main as base and adding rate limit l…
Harsh-Codes-77 May 19, 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
49 changes: 49 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: E2E

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
workflow_dispatch:

concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true

jobs:
playwright:
name: Playwright smoke tests
runs-on: ubuntu-latest
env:
NEXTAUTH_SECRET: playwright-placeholder-secret-that-is-long-enough
NEXTAUTH_URL: http://127.0.0.1:3000
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3000
GITHUB_ID: playwright-github-id
GITHUB_SECRET: playwright-github-secret
NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder-anon-key
SUPABASE_SERVICE_ROLE_KEY: placeholder-service-role-key
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install app dependencies
run: npm ci

- name: Install Playwright browsers
run: npx -y @playwright/test@1.49.1 install --with-deps chromium

- name: Run Playwright tests
run: npx -y @playwright/test@1.49.1 test

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ NEXTAUTH_SECRET=run_openssl_rand_base64_32

GITHUB_ID=your_client_id
GITHUB_SECRET=your_client_secret
GITHUB_WEBHOOK_SECRET=your_random_webhook_secret
```

### 5. Run
Expand All @@ -133,6 +134,17 @@ npm run dev

Visit `http://localhost:3000`.

### GitHub Webhook Refresh

DevTrack can accept GitHub push webhooks at `/api/webhooks/github` to mark a user's metrics for refresh as soon as new commits land.

1. Generate `GITHUB_WEBHOOK_SECRET` and add it to your deployment environment.
2. In the GitHub repository, open **Settings -> Webhooks -> Add webhook**.
3. Set the payload URL to `{NEXTAUTH_URL}/api/webhooks/github`.
4. Set content type to `application/json`, paste the same secret, and select the **Push** event.

Webhook requests are verified with GitHub's `X-Hub-Signature-256` HMAC header before DevTrack touches user metrics.

---

## Contributing
Expand Down
32 changes: 32 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ Out of scope:
- Social engineering attacks
- Rate limiting / denial of service on free-tier Vercel/Supabase

## Row Level Security (RLS)

DevTrack uses Supabase with Row Level Security enabled on all tables to ensure users can only access their own data.

### Protected Tables

| Table | RLS Enabled | Policies |
|-------|-------------|----------|
| `users` | ✅ | SELECT, UPDATE own row only |
| `goals` | ✅ | SELECT, INSERT, UPDATE, DELETE own rows only |
| `metric_snapshots` | ✅ | SELECT, INSERT, DELETE own rows only |

### How It Works

- All RLS policies use `auth.uid()` to match against the `id` or `user_id` column
- Users can only read, write, or delete their **own** rows
- `supabaseAdmin` (service role key) bypasses RLS automatically for trusted server-side operations — it is **never** exposed to the client
- The anon key has no access to any table by default

### Migration

RLS policies are defined in:

## Disclosure Policy

Once a fix is released, we will publish a summary in the [GitHub Security Advisories](https://github.com/Priyanshu-byte-coder/devtrack/security/advisories) page. Credit will be given to the reporter unless they prefer to remain anonymous.

To apply locally:
```bash
supabase db push
```

### Security Principle

All client-facing queries use the anon key with RLS enforcement. Server-side API routes use `supabaseAdmin` only when elevated privileges are required (e.g. creating a user on first login).
21 changes: 21 additions & 0 deletions e2e/landing.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, test } from "@playwright/test";

test("landing page renders GitHub sign-in entrypoint", async ({ page }) => {
await page.goto("/");

await expect(page.getByRole("heading", { name: "DevTrack" })).toBeVisible();
await expect(
page.getByRole("link", { name: "Sign in with GitHub" }),
).toHaveAttribute("href", /\/api\/auth\/signin\/github\?callbackUrl=\/dashboard/);
await expect(page.getByRole("link", { name: "View on GitHub" })).toHaveAttribute(
"href",
"https://github.com/Priyanshu-byte-coder/devtrack",
);
});

test("dashboard stays protected for unauthenticated users", async ({ page }) => {
await page.goto("/dashboard");

await expect(page).toHaveURL(/\/$/);
await expect(page.getByRole("link", { name: "Sign in with GitHub" })).toBeVisible();
});
13 changes: 13 additions & 0 deletions e2e/public-profile.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect, test } from "@playwright/test";

test("public profile route renders without requiring authentication", async ({ page }) => {
await page.goto("/u/playwright-user");

await expect(page).toHaveURL(/\/u\/playwright-user$/);
await expect(
page.getByRole("heading", {
name: /(@playwright-user's Profile|Profile Not Found)/,
}),
).toBeVisible();
await expect(page.getByRole("link", { name: "Sign in with GitHub" })).toHaveCount(0);
});
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@supabase/supabase-js": "^2.43.4",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"html-to-image": "^1.11.13",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7",
"next": "^14.2.35",
Expand Down
1 change: 1 addition & 0 deletions src/app/api/metrics/issues/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { authOptions } from "@/lib/auth";
import { fetchIssuesMetrics } from "@/lib/github";

export const dynamic = "force-dynamic";
export const revalidate=300;

export async function GET() {
const session = await getServerSession(authOptions);
Expand Down
25 changes: 16 additions & 9 deletions src/app/api/metrics/repo-health/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,18 @@ async function fetchJson<T>(url: string, token: string, accept?: string): Promis

async function fetchSignalsForRepo(
token: string,
repoFullName: string
repoFullName: string,
days: number
): Promise<RepoHealthSignals> {
const since30 = new Date();
since30.setDate(since30.getDate() - 30);
const since30Str = toDateStr(since30);
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];

// a) commit frequency in last 30 days (sampled to 100 via per_page=100)
const commitSearch = await fetchJson<{
items: unknown[];
}>(
`${GITHUB_API}/search/commits?q=repo:${repoFullName}+committer-date:>${since30Str}&per_page=100&sort=committer-date&order=desc`,
`${GITHUB_API}/search/commits?q=repo:${repoFullName}+committer-date:>${since}&per_page=100&sort=committer-date&order=desc`,
token,
"application/vnd.github+json"
);
Expand All @@ -116,14 +117,14 @@ async function fetchSignalsForRepo(
total_count: number;
items: Array<{ created_at: string; closed_at: string | null }>;
}>(
`${GITHUB_API}/search/issues?q=repo:${repoFullName}+type:pr+created:>${since30Str}&per_page=100&sort=created&order=desc`,
`${GITHUB_API}/search/issues?q=repo:${repoFullName}+type:pr+created:>${since}&per_page=100&sort=created&order=desc`,
token
);

const mergedPrs = await fetchJson<{
total_count: number;
}>(
`${GITHUB_API}/search/issues?q=repo:${repoFullName}+type:pr+is:merged+merged:>${since30Str}&per_page=100&sort=updated&order=desc`,
`${GITHUB_API}/search/issues?q=repo:${repoFullName}+type:pr+is:merged+merged:>${since}&per_page=100&sort=updated&order=desc`,
token
);

Expand Down Expand Up @@ -170,10 +171,16 @@ export async function GET(req: NextRequest) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const requestedDays = parseInt(
req.nextUrl.searchParams.get("days") ?? "30", 10
);
const days = requestedDays === 7 || requestedDays === 30
|| requestedDays === 90 ? requestedDays : 30;

// 1) Determine top repos (top 6 by commit count).
let topRepos: RepoSummary[] = [];
try {
topRepos = (await fetchReposForAccount(session.accessToken, session.githubLogin, 30)).repos;
topRepos = (await fetchReposForAccount(session.accessToken, session.githubLogin, days)).repos;
} catch {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
Expand All @@ -183,7 +190,7 @@ export async function GET(req: NextRequest) {
// 2) Fetch per-repo signals sequentially to preserve rate limits.
for (const repo of topRepos) {
try {
const signals = await fetchSignalsForRepo(session.accessToken, repo.name);
const signals = await fetchSignalsForRepo(session.accessToken, repo.name, days);
scores.push(computeHealthScore(repo.name, signals));
} catch {
// Skip repo on any failure.
Expand Down
Loading