fix: Admin Insights actual crash — function props across server/client boundary (v0.55.3)

Server logs (thanks to the user pulling them) showed the real error:
"Functions cannot be passed directly to Client Components" — the server
component page was passing formatShortDate/formatMonth as a `formatDate`
prop into TimeSeriesChart ("use client"). Functions aren't serializable
across the RSC boundary; the two previous fixes (query hardening,
Promise.allSettled) were real improvements but not the actual cause of
the reported crash.

TimeSeriesChart now takes a plain `dateFormat: "day" | "month"` string
and formats internally — BarChart was never affected (its formatValue
prop is only ever used via its own default, never passed from the page).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-19 21:01:13 +02:00
parent 6eb759dd43
commit 37739699f9
6 changed files with 33 additions and 12 deletions
+5
View File
@@ -2,6 +2,11 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
## 0.55.3 — 2026-07-19 19:00
### Fixed
- Admin Insights page's actual crash, found from server logs: the page (a server component) was passing plain formatter functions as props into its chart components (client components) — React can't serialize functions across that boundary. Replaced with a plain string prop ("day" vs "month") formatted inside the client component instead.
## 0.55.2 — 2026-07-19 18:20 ## 0.55.2 — 2026-07-19 18:20
### Fixed ### Fixed
+2 -7
View File
@@ -34,11 +34,6 @@ function lastNMonths(n: number): string[] {
return out; return out;
} }
function formatMonth(m: string) {
const [y, mo] = m.split("-");
return new Date(Number(y), Number(mo) - 1, 1).toLocaleDateString(undefined, { month: "short", year: "2-digit" });
}
export default async function AdminInsightsPage() { export default async function AdminInsightsPage() {
const since = new Date(); const since = new Date();
since.setDate(since.getDate() - DAYS); since.setDate(since.getDate() - DAYS);
@@ -128,7 +123,7 @@ export default async function AdminInsightsPage() {
<CardDescription>Daily, last {DAYS} days</CardDescription> <CardDescription>Daily, last {DAYS} days</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TimeSeriesChart data={signupSeries} formatDate={formatShortDate} /> <TimeSeriesChart data={signupSeries} dateFormat="day" />
</CardContent> </CardContent>
</Card> </Card>
@@ -168,7 +163,7 @@ export default async function AdminInsightsPage() {
<CardDescription>Monthly total across all users, last 6 months</CardDescription> <CardDescription>Monthly total across all users, last 6 months</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TimeSeriesChart data={usageSeries} formatDate={formatMonth} /> <TimeSeriesChart data={usageSeries} dateFormat="month" />
</CardContent> </CardContent>
</Card> </Card>
@@ -5,22 +5,36 @@ import { cn } from "@/lib/utils";
export type TimeSeriesPoint = { date: string; value: number }; export type TimeSeriesPoint = { date: string; value: number };
/** "day" expects "YYYY-MM-DD", "month" expects "YYYY-MM" — kept as a plain
* string enum rather than a formatter function prop, since this chart is
* a "use client" component and functions passed from a server component
* parent aren't serializable across that boundary (React throws: "Functions
* cannot be passed directly to Client Components"). */
function formatPoint(date: string, kind: "day" | "month"): string {
if (kind === "month") {
const [y, m] = date.split("-");
return new Date(Number(y), Number(m) - 1, 1).toLocaleDateString(undefined, { month: "short", year: "2-digit" });
}
return new Date(`${date}T00:00:00Z`).toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone: "UTC" });
}
/** Single-series area/line chart with a hover crosshair+tooltip and a /** Single-series area/line chart with a hover crosshair+tooltip and a
* table-view fallback — hand-rolled SVG, no charting lib. */ * table-view fallback — hand-rolled SVG, no charting lib. */
export function TimeSeriesChart({ export function TimeSeriesChart({
data, data,
formatValue = (n) => String(n), formatValue = (n) => String(n),
formatDate = (d) => d, dateFormat = "day",
height = 200, height = 200,
}: { }: {
data: TimeSeriesPoint[]; data: TimeSeriesPoint[];
formatValue?: (n: number) => string; formatValue?: (n: number) => string;
formatDate?: (d: string) => string; dateFormat?: "day" | "month";
height?: number; height?: number;
}) { }) {
const [hovered, setHovered] = useState<number | null>(null); const [hovered, setHovered] = useState<number | null>(null);
const [showTable, setShowTable] = useState(false); const [showTable, setShowTable] = useState(false);
const svgRef = useRef<SVGSVGElement>(null); const svgRef = useRef<SVGSVGElement>(null);
const formatDate = (d: string) => formatPoint(d, dateFormat);
const width = 600; const width = 600;
const paddingBottom = 24; const paddingBottom = 24;
+8 -1
View File
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together. // Mirrors CHANGELOG.md at the repo root — update both together.
export const APP_VERSION = "0.55.2"; export const APP_VERSION = "0.55.3";
export type ChangelogEntry = { export type ChangelogEntry = {
version: string; version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
}; };
export const CHANGELOG: ChangelogEntry[] = [ export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.55.3",
date: "2026-07-19 19:00",
fixed: [
"Admin Insights page's actual crash, found from server logs: the page (a server component) was passing plain formatter functions as props into its chart components (client components) — React can't serialize functions across that boundary. Replaced with a plain string prop (\"day\" vs \"month\") formatted inside the client component instead.",
],
},
{ {
version: "0.55.2", version: "0.55.2",
date: "2026-07-19 18:20", date: "2026-07-19 18:20",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@epicure/web", "name": "@epicure/web",
"version": "0.55.2", "version": "0.55.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "epicure", "name": "epicure",
"version": "0.55.2", "version": "0.55.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "pnpm --filter web dev", "dev": "pnpm --filter web dev",