Joey Eamigh 224a9046fc
fix: add "use cache" directives, fix server-client icon serialization
- Add Next.js 16 "use cache" with cacheLife profiles: seedData (1h),
  prices (5min), demand (5min), commodities (30min), ticker (1min),
  alerts (2min)
- Add cacheTag for parameterized server actions
- Fix MetricCard icon prop: pass rendered JSX instead of component
  references across the server-client boundary
2026-02-11 13:29:47 -05:00

436 lines
12 KiB
TypeScript

'use server';
import { getLatestPrices, getPriceTrends, getRegionPriceHeatmap } from '@/generated/prisma/sql.js';
import { prisma } from '@/lib/db.js';
import { serialize } from '@/lib/superjson.js';
import { validateRegionCode } from '@/lib/utils.js';
import { cacheLife, cacheTag } from 'next/cache';
type TimeRange = '24h' | '7d' | '30d' | '90d' | '1y';
function timeRangeToStartDate(range: TimeRange): Date {
const now = new Date();
const ms: Record<TimeRange, number> = {
'24h': 24 * 60 * 60 * 1000,
'7d': 7 * 24 * 60 * 60 * 1000,
'30d': 30 * 24 * 60 * 60 * 1000,
'90d': 90 * 24 * 60 * 60 * 1000,
'1y': 365 * 24 * 60 * 60 * 1000,
};
return new Date(now.getTime() - ms[range]);
}
interface ActionSuccess<T> {
ok: true;
data: ReturnType<typeof serialize<T>>;
}
interface ActionError {
ok: false;
error: string;
}
type ActionResult<T> = ActionSuccess<T> | ActionError;
export async function fetchLatestPrices(): Promise<ActionResult<getLatestPrices.Result[]>> {
'use cache';
cacheLife('prices');
cacheTag('latest-prices');
try {
const rows = await prisma.$queryRawTyped(getLatestPrices());
return { ok: true, data: serialize(rows) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch latest prices: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function fetchPriceTrends(
regionCode: string,
timeRange: TimeRange = '30d',
): Promise<ActionResult<getPriceTrends.Result[]>> {
'use cache';
cacheLife('prices');
cacheTag(`price-trends-${regionCode}-${timeRange}`);
try {
if (!validateRegionCode(regionCode)) {
return { ok: false, error: `Invalid region code: ${regionCode}` };
}
const startDate = timeRangeToStartDate(timeRange);
const endDate = new Date();
const rows = await prisma.$queryRawTyped(getPriceTrends(regionCode, startDate, endDate));
return { ok: true, data: serialize(rows) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch price trends: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function fetchPriceHeatmapData(): Promise<ActionResult<getRegionPriceHeatmap.Result[]>> {
'use cache';
cacheLife('prices');
cacheTag('price-heatmap');
try {
const rows = await prisma.$queryRawTyped(getRegionPriceHeatmap());
return { ok: true, data: serialize(rows) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch price heatmap data: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function fetchAllRegionPriceTrends(
timeRange: TimeRange = '30d',
): Promise<ActionResult<getPriceTrends.Result[]>> {
'use cache';
cacheLife('prices');
cacheTag(`all-price-trends-${timeRange}`);
try {
const startDate = timeRangeToStartDate(timeRange);
const endDate = new Date();
const regions = await prisma.gridRegion.findMany({ select: { code: true } });
const results = await Promise.all(
regions.map(r => prisma.$queryRawTyped(getPriceTrends(r.code, startDate, endDate))),
);
return { ok: true, data: serialize(results.flat()) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch all region price trends: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function fetchCommodityTrends(timeRange: TimeRange = '30d'): Promise<
ActionResult<
Array<{
commodity: string;
price: number;
unit: string;
timestamp: Date;
source: string;
}>
>
> {
'use cache';
cacheLife('commodities');
cacheTag(`commodity-trends-${timeRange}`);
try {
const startDate = timeRangeToStartDate(timeRange);
const rows = await prisma.commodityPrice.findMany({
where: { timestamp: { gte: startDate } },
orderBy: { timestamp: 'asc' },
});
return { ok: true, data: serialize(rows) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch commodity trends: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function fetchRegionCapacityVsPrice(): Promise<
ActionResult<
Array<{
region_code: string;
avg_price: number;
total_capacity_mw: number;
}>
>
> {
'use cache';
cacheLife('prices');
cacheTag('capacity-vs-price');
try {
const regions = await prisma.gridRegion.findMany({
select: {
code: true,
datacenters: { select: { capacityMw: true } },
electricityPrices: {
select: { priceMwh: true },
orderBy: { timestamp: 'desc' },
take: 100,
},
},
});
const result = regions.map(r => ({
region_code: r.code,
total_capacity_mw: r.datacenters.reduce((sum, d) => sum + d.capacityMw, 0),
avg_price:
r.electricityPrices.length > 0
? r.electricityPrices.reduce((sum, p) => sum + p.priceMwh, 0) / r.electricityPrices.length
: 0,
}));
return { ok: true, data: serialize(result) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch region capacity vs price: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function fetchPriceSparklines(): Promise<
ActionResult<
Array<{
region_code: string;
points: { value: number }[];
}>
>
> {
'use cache';
cacheLife('prices');
cacheTag('price-sparklines');
try {
const startDate = timeRangeToStartDate('7d');
const endDate = new Date();
const regions = await prisma.gridRegion.findMany({ select: { code: true } });
const results = await Promise.all(
regions.map(async r => {
const rows = await prisma.$queryRawTyped(getPriceTrends(r.code, startDate, endDate));
return {
region_code: r.code,
points: rows.map(row => ({ value: row.price_mwh })),
};
}),
);
return { ok: true, data: serialize(results) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch price sparklines: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function fetchRecentAlerts(): Promise<
ActionResult<
Array<{
id: string;
type: 'price_spike' | 'demand_peak' | 'price_drop';
severity: 'critical' | 'warning' | 'info';
region_code: string;
region_name: string;
description: string;
value: number;
unit: string;
timestamp: Date;
}>
>
> {
'use cache';
cacheLife('alerts');
cacheTag('recent-alerts');
try {
const since = timeRangeToStartDate('7d');
const priceRows = await prisma.electricityPrice.findMany({
where: { timestamp: { gte: since } },
orderBy: { timestamp: 'desc' },
take: 500,
include: { region: { select: { code: true, name: true } } },
});
const alerts: Array<{
id: string;
type: 'price_spike' | 'demand_peak' | 'price_drop';
severity: 'critical' | 'warning' | 'info';
region_code: string;
region_name: string;
description: string;
value: number;
unit: string;
timestamp: Date;
}> = [];
// Detect price spikes (above $80/MWh) and demand peaks
const regionAvgs = new Map<string, { sum: number; count: number }>();
for (const row of priceRows) {
const entry = regionAvgs.get(row.region.code) ?? { sum: 0, count: 0 };
entry.sum += row.priceMwh;
entry.count += 1;
regionAvgs.set(row.region.code, entry);
}
for (const row of priceRows) {
const avg = regionAvgs.get(row.region.code);
const avgPrice = avg ? avg.sum / avg.count : 0;
if (row.priceMwh >= 100) {
alerts.push({
id: `spike-${row.id}`,
type: 'price_spike',
severity: 'critical',
region_code: row.region.code,
region_name: row.region.name,
description: `${row.region.code} electricity hit $${row.priceMwh.toFixed(2)}/MWh`,
value: row.priceMwh,
unit: '$/MWh',
timestamp: row.timestamp,
});
} else if (row.priceMwh >= 80) {
alerts.push({
id: `spike-${row.id}`,
type: 'price_spike',
severity: 'warning',
region_code: row.region.code,
region_name: row.region.name,
description: `${row.region.code} electricity at $${row.priceMwh.toFixed(2)}/MWh`,
value: row.priceMwh,
unit: '$/MWh',
timestamp: row.timestamp,
});
} else if (avgPrice > 0 && row.priceMwh < avgPrice * 0.7) {
alerts.push({
id: `drop-${row.id}`,
type: 'price_drop',
severity: 'info',
region_code: row.region.code,
region_name: row.region.name,
description: `${row.region.code} price dropped to $${row.priceMwh.toFixed(2)}/MWh (avg $${avgPrice.toFixed(2)})`,
value: row.priceMwh,
unit: '$/MWh',
timestamp: row.timestamp,
});
}
if (row.demandMw >= 50000) {
alerts.push({
id: `demand-${row.id}`,
type: 'demand_peak',
severity: row.demandMw >= 70000 ? 'critical' : 'warning',
region_code: row.region.code,
region_name: row.region.name,
description: `${row.region.code} demand peaked at ${(row.demandMw / 1000).toFixed(1)} GW`,
value: row.demandMw,
unit: 'MW',
timestamp: row.timestamp,
});
}
}
// Sort by timestamp desc and limit
alerts.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
const limited = alerts.slice(0, 50);
return { ok: true, data: serialize(limited) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch recent alerts: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export interface TickerPriceRow {
region_code: string;
price_mwh: number;
prev_price_mwh: number | null;
}
export interface TickerCommodityRow {
commodity: string;
price: number;
prev_price: number | null;
unit: string;
}
export async function fetchTickerPrices(): Promise<
ActionResult<{ electricity: TickerPriceRow[]; commodities: TickerCommodityRow[] }>
> {
'use cache';
cacheLife('ticker');
cacheTag('ticker-prices');
try {
// Get the two most recent prices per region using a window function via raw SQL
const electricityRows = await prisma.$queryRaw<
Array<{ region_code: string; price_mwh: number; prev_price_mwh: number | null }>
>`
SELECT region_code, price_mwh, prev_price_mwh
FROM (
SELECT
r.code AS region_code,
ep.price_mwh,
LAG(ep.price_mwh) OVER (PARTITION BY ep.region_id ORDER BY ep.timestamp ASC) AS prev_price_mwh,
ROW_NUMBER() OVER (PARTITION BY ep.region_id ORDER BY ep.timestamp DESC) AS rn
FROM electricity_prices ep
JOIN grid_regions r ON ep.region_id = r.id
) sub
WHERE rn = 1
`;
// Get the two most recent commodity prices per commodity
const commodityRows = await prisma.$queryRaw<
Array<{ commodity: string; price: number; prev_price: number | null; unit: string }>
>`
SELECT commodity, price, prev_price, unit
FROM (
SELECT
commodity,
price,
unit,
LAG(price) OVER (PARTITION BY commodity ORDER BY timestamp ASC) AS prev_price,
ROW_NUMBER() OVER (PARTITION BY commodity ORDER BY timestamp DESC) AS rn
FROM commodity_prices
) sub
WHERE rn = 1
`;
return {
ok: true,
data: serialize({ electricity: electricityRows, commodities: commodityRows }),
};
} catch (err) {
return {
ok: false,
error: `Failed to fetch ticker prices: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function fetchLatestCommodityPrices(): Promise<
ActionResult<
Array<{
commodity: string;
price: number;
unit: string;
timestamp: Date;
source: string;
}>
>
> {
'use cache';
cacheLife('commodities');
cacheTag('latest-commodities');
try {
const rows = await prisma.commodityPrice.findMany({
orderBy: { timestamp: 'desc' },
distinct: ['commodity'],
});
return { ok: true, data: serialize(rows) };
} catch (err) {
return {
ok: false,
error: `Failed to fetch commodity prices: ${err instanceof Error ? err.message : String(err)}`,
};
}
}