'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 = { '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 { ok: true; data: ReturnType>; } interface ActionError { ok: false; error: string; } type ActionResult = ActionSuccess | ActionError; export async function fetchLatestPrices(): Promise> { '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> { '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> { '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> { '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(); 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)}`, }; } }