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.