diff --git a/src/app/dashboard/contributor/ContributorDashboardClient.tsx b/src/app/dashboard/contributor/ContributorDashboardClient.tsx index 5343c3a..e635b3b 100644 --- a/src/app/dashboard/contributor/ContributorDashboardClient.tsx +++ b/src/app/dashboard/contributor/ContributorDashboardClient.tsx @@ -19,7 +19,7 @@ import { apiRequest, fetchBounties } from "@/lib/api"; import { mockReputationProfiles, mockBounties, - recentActivity, + getRecentActivity, contributorEarningsHistory, contributorSparkline, } from "@/lib/mock-data"; @@ -277,7 +277,7 @@ export default function ContributorDashboardClient() { {isLive && }
- +
diff --git a/src/app/dashboard/maintainer/page.tsx b/src/app/dashboard/maintainer/page.tsx index 3d9fb5c..e538e5b 100644 --- a/src/app/dashboard/maintainer/page.tsx +++ b/src/app/dashboard/maintainer/page.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { Layers, Clock, Coins, FolderGit2, CheckCircle2 } from "lucide-react"; import { fetchBounties } from "@/lib/api"; -import { mockBounties, recentActivity, bountiesCompletedSparkline } from "@/lib/mock-data"; +import { mockBounties, getRecentActivity, bountiesCompletedSparkline } from "@/lib/mock-data"; import { DashboardShell } from "@/components/dashboard/DashboardShell"; import { ActivityList } from "@/components/dashboard/ActivityList"; import { StatCard } from "@/components/ui/StatCard"; @@ -167,7 +167,7 @@ export default async function MaintainerDashboardPage() { {source === "mock" && }
- +
diff --git a/src/app/dashboard/sponsor/SponsorDashboardClient.tsx b/src/app/dashboard/sponsor/SponsorDashboardClient.tsx index a9eae96..e459444 100644 --- a/src/app/dashboard/sponsor/SponsorDashboardClient.tsx +++ b/src/app/dashboard/sponsor/SponsorDashboardClient.tsx @@ -19,7 +19,7 @@ import { adaptBounty, type RawBounty, type RawMilestone } from "@/lib/adapters"; import { mockSponsorSummary, mockBounties, - recentActivity, + getRecentActivity, sponsorSpendHistory, sponsorSpendByRepo, budgetSparkline, @@ -284,7 +284,7 @@ export default function SponsorDashboardClient() { {isLive && }
- +
{/* Dev-only: visual kitchen sink — renders all four StatCard states diff --git a/src/app/page.tsx b/src/app/page.tsx index 93bd35a..e98fcaa 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -22,7 +22,7 @@ import { platformStats, trustedOrgs, topContributors, - recentActivity, + getRecentActivity, faqs, } from "@/lib/mock-data"; import { formatCurrency, formatInteger } from "@/lib/utils"; @@ -270,7 +270,7 @@ export default function HomePage() {
- +
diff --git a/src/lib/mock-data.test.ts b/src/lib/mock-data.test.ts index 1bd0ec5..bfc2a0d 100644 --- a/src/lib/mock-data.test.ts +++ b/src/lib/mock-data.test.ts @@ -2,7 +2,7 @@ import { mockBounties, mockMaintenancePools, mockMilestones, - recentActivity, + getRecentActivity, } from "./mock-data"; import { formatCurrency, isPlausibleAmount } from "./utils"; @@ -13,7 +13,7 @@ describe("mock-data asset coverage", () => { mockBounties, mockMilestones, mockMaintenancePools, - recentActivity: recentActivity.filter((e) => e.asset), + recentActivity: getRecentActivity().filter((e) => e.asset), }; it.each(Object.entries(collections))("%s contains both USDC and XLM entries", (_, items) => { @@ -26,13 +26,27 @@ describe("mock-data asset coverage", () => { ...mockBounties.map((b) => b.reward), ...mockMilestones.flatMap((m) => [m.budget, m.distributed]), ...mockMaintenancePools.flatMap((p) => [p.balance, p.monthlyDeposit]), - ...recentActivity.flatMap((e) => (e.amount === undefined ? [] : [e.amount])), + ...getRecentActivity().flatMap((e) => (e.amount === undefined ? [] : [e.amount])), ]; for (const amount of amounts) { expect(isPlausibleAmount(amount)).toBe(true); } }); + it("anchors activity timestamps to the current call time, not module load (#707)", () => { + jest.useFakeTimers().setSystemTime(new Date("2026-01-01T00:00:00.000Z")); + const first = getRecentActivity()[0].occurredAt; + + // Simulate a long-running server: real time passes well after the + // module was first evaluated. + jest.setSystemTime(new Date("2026-01-03T00:00:00.000Z")); + const twoDaysLater = getRecentActivity()[0].occurredAt; + + jest.useRealTimers(); + expect(twoDaysLater).not.toBe(first); + expect(new Date(twoDaysLater).getTime()).toBeGreaterThan(new Date(first).getTime()); + }); + it("XLM mock amounts render with XLM precision and no sanity warning", () => { const pool = mockMaintenancePools.find((p) => p.asset === "XLM")!; expect(formatCurrency(pool.balance, pool.asset)).toBe("8,412.1234567 XLM"); diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts index de0dab0..9b521a0 100644 --- a/src/lib/mock-data.ts +++ b/src/lib/mock-data.ts @@ -275,65 +275,73 @@ function minutesAgo(n: number): string { return new Date(Date.now() - n * 60_000).toISOString(); } -export const recentActivity: ActivityEvent[] = [ - { - id: "a1", - handle: "devrel_ana", - action: "was paid", - target: "core-indexer#288", - amount: 40, - asset: "USDC", - occurredAt: minutesAgo(6), - }, - { - id: "a2", - handle: "0xkoda", - action: "claimed", - target: "soroban-escrow-sdk#17", - occurredAt: minutesAgo(22), - }, - { - id: "a3", - handle: "linh_dev", - action: "opened a pull request for", - target: "stellar-wallet-kit#475", - occurredAt: minutesAgo(48), - }, - { - id: "a4", - handle: "Stellar Development Foundation", - action: "funded", - target: "v2.0: Multi-asset escrow", - amount: 2500, - asset: "USDC", - occurredAt: minutesAgo(71), - }, - { - id: "a4b", - handle: "Stellar Community Fund", - action: "funded", - target: "v1.4: Indexer performance pass", - amount: 15000, - asset: "XLM", - occurredAt: minutesAgo(95), - }, - { - id: "a5", - handle: "priyaeth", - action: "was paid", - target: "core-indexer#301", - amount: 480, - asset: "USDC", - occurredAt: minutesAgo(130), - }, - { - id: "a6", - handle: "qa_marcus", - action: "joined as a", - target: "contributor", - occurredAt: minutesAgo(210), - }, -]; +// A function, not a module-level constant: minutesAgo(n) anchors each event +// n minutes before whenever it's *called*. A constant array only calls +// minutesAgo once, when the module first loads -- on a long-running server +// that's once at deploy, so the "live" feed goes stale for the rest of the +// process's uptime (#707). Calling getRecentActivity() at render/request +// time instead keeps every event's anchor fresh relative to now. +export function getRecentActivity(): ActivityEvent[] { + return [ + { + id: "a1", + handle: "devrel_ana", + action: "was paid", + target: "core-indexer#288", + amount: 40, + asset: "USDC", + occurredAt: minutesAgo(6), + }, + { + id: "a2", + handle: "0xkoda", + action: "claimed", + target: "soroban-escrow-sdk#17", + occurredAt: minutesAgo(22), + }, + { + id: "a3", + handle: "linh_dev", + action: "opened a pull request for", + target: "stellar-wallet-kit#475", + occurredAt: minutesAgo(48), + }, + { + id: "a4", + handle: "Stellar Development Foundation", + action: "funded", + target: "v2.0: Multi-asset escrow", + amount: 2500, + asset: "USDC", + occurredAt: minutesAgo(71), + }, + { + id: "a4b", + handle: "Stellar Community Fund", + action: "funded", + target: "v1.4: Indexer performance pass", + amount: 15000, + asset: "XLM", + occurredAt: minutesAgo(95), + }, + { + id: "a5", + handle: "priyaeth", + action: "was paid", + target: "core-indexer#301", + amount: 480, + asset: "USDC", + occurredAt: minutesAgo(130), + }, + { + id: "a6", + handle: "qa_marcus", + action: "joined as a", + target: "contributor", + occurredAt: minutesAgo(210), + }, + ]; +} // Weekly earnings/spend series (8 weeks) for dashboard charts.