Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 7 additions & 28 deletions src/app/api/leaderboard/rebuild/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// @ts-nocheck
import { NextRequest, NextResponse } from "next/server";
import { buildLeaderboard, setMemoryCachedLeaderboard, CACHE_STALE_SECONDS, LEADERBOARD_CACHE_KEY } from "@/lib/leaderboard";
import { cacheSet } from "@/lib/metrics-cache";
import { supabaseAdmin, isSupabaseAdminAvailable } from "@/lib/supabase";
import { refreshAllLeaderboardCaches } from "@/lib/leaderboard";

export async function POST(req: NextRequest) {
const token = req.headers.get("x-devtrack-rebuild-token") ?? req.nextUrl.searchParams.get("token");
Expand All @@ -12,31 +10,12 @@ export async function POST(req: NextRequest) {
}

try {
const payload = await buildLeaderboard();
await cacheSet(LEADERBOARD_CACHE_KEY, payload, CACHE_STALE_SECONDS);
setMemoryCachedLeaderboard(payload);

if (isSupabaseAdminAvailable) {
try {
const now = new Date().toISOString();
const expiresAt = new Date(Date.now() + CACHE_STALE_SECONDS * 1000).toISOString();
await supabaseAdmin.from("leaderboard_cache").upsert(
{
key: LEADERBOARD_CACHE_KEY,
payload,
generated_at: now,
expires_at: expiresAt,
building_until: null,
updated_at: now,
},
{ onConflict: "key" }
);
} catch (err) {
console.warn("[Leaderboard] Failed to persist cache to Supabase during rebuild:", err);
}
}

return NextResponse.json({ ok: true, generatedAt: payload.generatedAt });
const payloads = await refreshAllLeaderboardCaches();
return NextResponse.json({
ok: true,
generatedAt: payloads.weekly.generatedAt,
timeframes: Object.keys(payloads),
});
} catch (err) {
console.error("[Leaderboard] Rebuild failed:", err);
return NextResponse.json({ error: "Rebuild failed" }, { status: 500 });
Expand Down
10 changes: 7 additions & 3 deletions src/app/api/leaderboard/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { refreshLeaderboardCache } from "@/lib/leaderboard";
import { refreshAllLeaderboardCaches } from "@/lib/leaderboard";

export const dynamic = "force-dynamic";

Expand All @@ -16,8 +16,12 @@ export async function GET(req: Request) {
}

try {
const payload = await refreshLeaderboardCache();
return NextResponse.json({ success: true, generatedAt: payload.generatedAt });
const payloads = await refreshAllLeaderboardCaches();
return NextResponse.json({
success: true,
generatedAt: payloads.weekly.generatedAt,
timeframes: Object.keys(payloads),
});
} catch (err) {
return NextResponse.json({ error: "Failed to refresh leaderboard cache" }, { status: 500 });
}
Expand Down
47 changes: 36 additions & 11 deletions src/app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {
getLeaderboardData,
isFresh,
LEADERBOARD_BUILD_LOCK_KEY,
normalizeLeaderboardTimeframe,
type LeaderboardPayload,
type LeaderboardPeriod,
filterLeaderboardByLanguage,
type LeaderboardTimeframe,
} from "@/lib/leaderboard";
import {
pruneExpiredRateLimits,
Expand Down Expand Up @@ -75,19 +76,32 @@ function normalizeLanguage(value: string | null): string | undefined {
return normalized || undefined;
}

function normalizePeriod(value: string | null): LeaderboardPeriod {
if (value === "week" || value === "month" || value === "all") {
return value;
function normalizeTimeframe(
timeframe: string | null,
period: string | null
): LeaderboardTimeframe | null {
const normalizedTimeframe = normalizeLeaderboardTimeframe(timeframe);
if (timeframe !== null && normalizedTimeframe === null) {
return null;
}

return "all";
if (normalizedTimeframe) {
return normalizedTimeframe;
}

const normalizedPeriod = normalizeLeaderboardTimeframe(period);
if (period !== null && normalizedPeriod === null) {
return null;
}

return normalizedPeriod ?? "weekly";
}

function getLanguageCacheKey(filters: {
language: string;
period: LeaderboardPeriod;
timeframe: LeaderboardTimeframe;
}): string {
return `${getBaseLeaderboardCacheKey(filters.period)}:${filters.language}`;
return `${getBaseLeaderboardCacheKey(filters.timeframe)}:${filters.language}`;
}

function getLeaderboardBuildLockCacheKey(cacheKey: string): string {
Expand All @@ -98,10 +112,21 @@ export async function GET(req: NextRequest) {
const ip = getRateLimitKey(req);
const rateLimit = await checkRateLimit(ip);
const language = normalizeLanguage(req.nextUrl.searchParams.get("lang"));
const period = normalizePeriod(req.nextUrl.searchParams.get("period"));
const timeframe = normalizeTimeframe(
req.nextUrl.searchParams.get("timeframe"),
req.nextUrl.searchParams.get("period")
);

if (!timeframe) {
return NextResponse.json(
{ error: "Invalid timeframe. Supported values are weekly, monthly, and all_time." },
{ status: 400 }
);
}

const cacheKey = language
? getLanguageCacheKey({ language, period })
: getBaseLeaderboardCacheKey(period);
? getLanguageCacheKey({ language, timeframe })
: getBaseLeaderboardCacheKey(timeframe);

if (!rateLimit.allowed) {
return NextResponse.json(
Expand Down Expand Up @@ -145,7 +170,7 @@ export async function GET(req: NextRequest) {
}

try {
const baseLeaderboard = await getLeaderboardData(bypass, { period });
const baseLeaderboard = await getLeaderboardData(bypass, { timeframe });

if (!baseLeaderboard) {
const stale = await cacheGet<LeaderboardPayload>(cacheKey);
Expand Down
2 changes: 2 additions & 0 deletions src/app/leaderboard/LeaderboardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { Skeleton } from "@/components/ui/skeleton";

const ROWS = 8;
Expand Down
8 changes: 4 additions & 4 deletions src/app/leaderboard/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ export default function Loading() {
</div>
</div>

{/* Tabs Skeleton */}
{/* Metric Tabs Skeleton */}
<div className="mb-6 flex flex-wrap gap-2 rounded-2xl border border-[var(--border)] bg-[var(--card)]/90 p-2 shadow-[var(--shadow-soft)]">
{[1, 2, 3].map((i) => (
<div key={i} className="h-10 w-24 rounded-lg bg-[var(--card-muted)] animate-pulse" />
))}
</div>

{/* Filters Skeleton */}
<div className="mb-6 flex gap-3">
<div className="h-10 w-32 rounded-lg bg-[var(--card-muted)] animate-pulse" />
<div className="h-10 w-32 rounded-lg bg-[var(--card-muted)] animate-pulse" />
<div className="mb-6 flex items-center justify-between gap-3 rounded-2xl border border-[var(--border)] bg-[var(--card)]/90 p-3 shadow-[var(--shadow-soft)]">
<div className="h-10 w-40 rounded-lg bg-[var(--card-muted)] animate-pulse" />
<div className="h-10 w-28 rounded-lg bg-[var(--card-muted)] animate-pulse" />
</div>

{/* Table Skeleton */}
Expand Down
Loading
Loading