phase 1: foundation — next.js 16 scaffold, prisma schema, docker compose, seed data

This commit is contained in:
Joey Eamigh 2026-02-11 04:21:20 -05:00
parent 1e9b287036
commit 6d7d2d966b
No known key found for this signature in database
GPG Key ID: CE8C05DFFC53C9CB
32 changed files with 3050 additions and 22 deletions

51
.gitignore vendored Normal file
View File

@ -0,0 +1,51 @@
# dependencies
node_modules/
.pnp
.pnp.js
# next.js
.next/
out/
# production
build/
dist/
# environment
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# database
pgdata/
# misc
.DS_Store
*.pem
.idea/
.vscode/
# prisma
src/generated/
# bun
*.tgz
# code coverage
coverage/
*.lcov
# caches
.eslintcache
.cache

View File

@ -7,7 +7,7 @@ export default {
arrowParens: 'avoid',
semi: true,
plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
tailwindStylesheet: './app/globals.css',
tailwindStylesheet: './src/app/globals.css',
overrides: [
{
files: ['*.ts', '*.js', '*.tsx', '*.jsx', '*.cjs', '*.mjs'],

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# bonus4
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run
```
This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

1188
bun.lock Normal file

File diff suppressed because it is too large Load Diff

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

122
data/ai-milestones.json Normal file
View File

@ -0,0 +1,122 @@
[
{
"date": "2020-06-11",
"title": "GPT-3 API Launch",
"description": "OpenAI opens GPT-3 API access, demonstrating that scaling language models yields emergent capabilities. Signals the start of the large-model era.",
"category": "model_launch"
},
{
"date": "2021-06-29",
"title": "GitHub Copilot Preview",
"description": "GitHub launches Copilot technical preview, the first mass-market AI coding assistant. Powered by OpenAI Codex, it demonstrates commercial viability of large language models.",
"category": "model_launch"
},
{
"date": "2022-04-06",
"title": "DALL-E 2 Launch",
"description": "OpenAI unveils DALL-E 2, a breakthrough in AI image generation. Kicks off the generative AI media wave and accelerates GPU demand for inference workloads.",
"category": "model_launch"
},
{
"date": "2022-08-22",
"title": "Stable Diffusion Open-Source Release",
"description": "Stability AI releases Stable Diffusion publicly under an open-source license, democratizing AI image generation and driving massive consumer GPU demand.",
"category": "model_launch"
},
{
"date": "2022-11-30",
"title": "ChatGPT Launch",
"description": "OpenAI releases ChatGPT, reaching 1 million users in 5 days and 100 million in 2 months. Sparks the generative AI boom and triggers an unprecedented race for GPU compute.",
"category": "model_launch"
},
{
"date": "2023-01-23",
"title": "Microsoft Invests $10B in OpenAI",
"description": "Microsoft announces a $10 billion investment in OpenAI, the largest single AI investment at the time. Signals hyperscaler conviction that AI will drive massive infrastructure spend.",
"category": "market"
},
{
"date": "2023-03-14",
"title": "GPT-4 Launch",
"description": "OpenAI releases GPT-4, a multimodal model accepting text and image inputs. Demonstrates a step-change in AI capability, intensifying the arms race for training compute.",
"category": "model_launch"
},
{
"date": "2023-05-30",
"title": "NVIDIA Hits $1 Trillion Market Cap",
"description": "NVIDIA becomes the first chipmaker to reach a $1 trillion market capitalization, driven by explosive AI GPU demand. Stock price reflects the market's bet on sustained datacenter buildout.",
"category": "market"
},
{
"date": "2023-07-18",
"title": "Meta Releases Llama 2 Open-Source",
"description": "Meta releases Llama 2 for research and commercial use, open-sourcing 7B, 13B, and 70B parameter models. Accelerates AI adoption by lowering barriers and increasing total compute demand.",
"category": "model_launch"
},
{
"date": "2023-08-17",
"title": "NVIDIA H100 GPU Shortage Peak",
"description": "Reports confirm H100 GPUs are sold out through Q1 2024 with 6+ month lead times. CoWoS packaging bottleneck limits supply to ~550K units for the year, driving datacenter expansion plans.",
"category": "infrastructure"
},
{
"date": "2023-10-30",
"title": "Biden Executive Order on AI Safety",
"description": "President Biden signs Executive Order 14110, the most comprehensive US AI governance action. Establishes safety standards, reporting requirements for large model training runs, and energy impact assessments.",
"category": "policy"
},
{
"date": "2023-12-06",
"title": "Google Gemini Launch",
"description": "Google announces Gemini 1.0 with Ultra, Pro, and Nano variants. Google's entry into the frontier model race further escalates industry-wide datacenter investment.",
"category": "model_launch"
},
{
"date": "2024-03-04",
"title": "Claude 3 Family Launch",
"description": "Anthropic releases Claude 3 Haiku, Sonnet, and Opus, the first AI model family with clear speed/cost/capability tradeoffs. Broadens enterprise AI adoption and inference compute demand.",
"category": "model_launch"
},
{
"date": "2024-03-18",
"title": "NVIDIA Blackwell GPU Architecture Announced",
"description": "NVIDIA unveils Blackwell B200 and GB200 GPUs at GTC 2024. Each GB200 rack draws up to 120 kW, signaling that next-generation AI hardware will dramatically increase datacenter power density.",
"category": "infrastructure"
},
{
"date": "2024-05-13",
"title": "GPT-4o Launch",
"description": "OpenAI releases GPT-4o (omni), a natively multimodal model processing text, image, and audio. Made free in ChatGPT, massively expanding inference volume and datacenter load.",
"category": "model_launch"
},
{
"date": "2024-07-15",
"title": "PJM Capacity Auction Sets Record Prices",
"description": "PJM Interconnection's capacity auction clears at $16.1 billion, up 9.5% year-over-year. Data center load growth identified as the primary driver, with 30 GW of new demand projected by 2030.",
"category": "market"
},
{
"date": "2024-12-18",
"title": "NERC Warns of Nationwide Capacity Shortfalls",
"description": "NERC's Long-Term Reliability Assessment warns that all US regions except MRO-Manitoba face elevated or high risk of supply shortfalls by 2028, driven primarily by datacenter load growth.",
"category": "policy"
},
{
"date": "2025-01-03",
"title": "Microsoft Commits $80B to AI Datacenters",
"description": "Microsoft announces plans to spend $80 billion on AI-enabled datacenters in fiscal year 2025, more than half in the US. Largest single-company infrastructure commitment in history.",
"category": "infrastructure"
},
{
"date": "2025-01-21",
"title": "Stargate Project Announced ($500B)",
"description": "OpenAI, SoftBank, and Oracle announce the Stargate Project: $500 billion over four years for AI infrastructure, with $100 billion deployed immediately. Represents a new scale of AI investment.",
"category": "infrastructure"
},
{
"date": "2025-11-12",
"title": "Anthropic Announces $50B Infrastructure Buildout",
"description": "Anthropic announces plans to spend $50 billion on US AI infrastructure, starting with custom datacenters in Texas and New York. Signals that AI labs themselves are becoming major power consumers.",
"category": "infrastructure"
}
]

485
data/datacenters.geojson Normal file
View File

@ -0,0 +1,485 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-77.4875, 39.0438] },
"properties": {
"name": "AWS US-East Ashburn Campus",
"operator": "AWS",
"capacity_mw": 350,
"status": "operational",
"year_opened": 2006,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-77.5105, 38.7515] },
"properties": {
"name": "AWS Manassas Data Center",
"operator": "AWS",
"capacity_mw": 200,
"status": "operational",
"year_opened": 2016,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-82.9071, 40.0798] },
"properties": {
"name": "AWS US-East-2 Columbus Campus",
"operator": "AWS",
"capacity_mw": 250,
"status": "operational",
"year_opened": 2016,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-119.8526, 45.6946] },
"properties": {
"name": "AWS US-West-2 Oregon Campus",
"operator": "AWS",
"capacity_mw": 300,
"status": "operational",
"year_opened": 2011,
"region": "BPA"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-86.2748, 40.4822] },
"properties": {
"name": "AWS Indiana Data Center Campus",
"operator": "AWS",
"capacity_mw": 525,
"status": "operational",
"year_opened": 2023,
"region": "MISO"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-121.1787, 45.5946] },
"properties": {
"name": "Google The Dalles Data Center",
"operator": "Google",
"capacity_mw": 250,
"status": "operational",
"year_opened": 2006,
"region": "BPA"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-95.8608, 41.2619] },
"properties": {
"name": "Google Council Bluffs Data Center",
"operator": "Google",
"capacity_mw": 400,
"status": "operational",
"year_opened": 2009,
"region": "MISO"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-95.2400, 36.3000] },
"properties": {
"name": "Google Mayes County Data Center",
"operator": "Google",
"capacity_mw": 300,
"status": "operational",
"year_opened": 2011,
"region": "SPP"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-77.4207, 36.6713] },
"properties": {
"name": "Google Loudoun County Data Center",
"operator": "Google",
"capacity_mw": 200,
"status": "operational",
"year_opened": 2019,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-86.5861, 33.3684] },
"properties": {
"name": "Google Bridgeport Data Center",
"operator": "Google",
"capacity_mw": 300,
"status": "under_construction",
"year_opened": 2025,
"region": "SERC"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-98.4936, 29.4241] },
"properties": {
"name": "Microsoft San Antonio Data Center",
"operator": "Microsoft",
"capacity_mw": 250,
"status": "operational",
"year_opened": 2010,
"region": "ERCOT"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-78.3877, 36.6559] },
"properties": {
"name": "Microsoft Boydton Data Center",
"operator": "Microsoft",
"capacity_mw": 400,
"status": "operational",
"year_opened": 2010,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-119.8530, 47.2343] },
"properties": {
"name": "Microsoft Quincy Data Center",
"operator": "Microsoft",
"capacity_mw": 300,
"status": "operational",
"year_opened": 2007,
"region": "BPA"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-93.7910, 41.5770] },
"properties": {
"name": "Microsoft West Des Moines Data Center",
"operator": "Microsoft",
"capacity_mw": 350,
"status": "operational",
"year_opened": 2014,
"region": "MISO"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-87.8135, 41.7375] },
"properties": {
"name": "Microsoft Chicago Data Center",
"operator": "Microsoft",
"capacity_mw": 150,
"status": "operational",
"year_opened": 2009,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-111.9749, 33.3784] },
"properties": {
"name": "Microsoft Phoenix (Goodyear) Data Center",
"operator": "Microsoft",
"capacity_mw": 250,
"status": "operational",
"year_opened": 2021,
"region": "SPP"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-120.9224, 44.2993] },
"properties": {
"name": "Meta Prineville Data Center",
"operator": "Meta",
"capacity_mw": 350,
"status": "operational",
"year_opened": 2011,
"region": "BPA"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-93.4747, 41.6432] },
"properties": {
"name": "Meta Altoona Data Center",
"operator": "Meta",
"capacity_mw": 400,
"status": "operational",
"year_opened": 2014,
"region": "MISO"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-97.2767, 32.8140] },
"properties": {
"name": "Meta Fort Worth Data Center",
"operator": "Meta",
"capacity_mw": 250,
"status": "operational",
"year_opened": 2016,
"region": "ERCOT"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-82.8085, 40.0747] },
"properties": {
"name": "Meta New Albany Data Center",
"operator": "Meta",
"capacity_mw": 300,
"status": "operational",
"year_opened": 2020,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-111.6780, 33.4484] },
"properties": {
"name": "Meta Mesa Data Center",
"operator": "Meta",
"capacity_mw": 200,
"status": "operational",
"year_opened": 2022,
"region": "SPP"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-77.4591, 39.0204] },
"properties": {
"name": "Digital Realty Ashburn ACC3 Campus",
"operator": "Digital Realty",
"capacity_mw": 150,
"status": "operational",
"year_opened": 2014,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-96.8560, 32.8970] },
"properties": {
"name": "Digital Realty Dallas Campus",
"operator": "Digital Realty",
"capacity_mw": 120,
"status": "operational",
"year_opened": 2005,
"region": "ERCOT"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-87.6503, 41.8495] },
"properties": {
"name": "Digital Realty Chicago Campus",
"operator": "Digital Realty",
"capacity_mw": 100,
"status": "operational",
"year_opened": 2007,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-121.7830, 37.2418] },
"properties": {
"name": "Equinix SV1 San Jose Campus",
"operator": "Equinix",
"capacity_mw": 80,
"status": "operational",
"year_opened": 2002,
"region": "CAISO"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-77.4877, 39.0296] },
"properties": {
"name": "Equinix DC11 Ashburn Campus",
"operator": "Equinix",
"capacity_mw": 100,
"status": "operational",
"year_opened": 2015,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-74.0566, 40.7831] },
"properties": {
"name": "Equinix NY5 Secaucus Campus",
"operator": "Equinix",
"capacity_mw": 60,
"status": "operational",
"year_opened": 2010,
"region": "NYISO"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-96.9365, 32.9290] },
"properties": {
"name": "Equinix DA1 Dallas Campus",
"operator": "Equinix",
"capacity_mw": 50,
"status": "operational",
"year_opened": 2000,
"region": "ERCOT"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-87.6298, 41.8535] },
"properties": {
"name": "Equinix CH1 Chicago Campus",
"operator": "Equinix",
"capacity_mw": 50,
"status": "operational",
"year_opened": 2001,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-96.8419, 32.9339] },
"properties": {
"name": "QTS Irving (Dallas) Data Center",
"operator": "QTS",
"capacity_mw": 110,
"status": "operational",
"year_opened": 2010,
"region": "ERCOT"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-77.4700, 39.0451] },
"properties": {
"name": "QTS Ashburn Data Center",
"operator": "QTS",
"capacity_mw": 130,
"status": "operational",
"year_opened": 2017,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-87.6579, 41.8458] },
"properties": {
"name": "QTS Chicago Data Center",
"operator": "QTS",
"capacity_mw": 80,
"status": "operational",
"year_opened": 2012,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-122.9907, 45.5412] },
"properties": {
"name": "QTS Hillsboro Data Center",
"operator": "QTS",
"capacity_mw": 70,
"status": "operational",
"year_opened": 2015,
"region": "BPA"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-96.8929, 32.9501] },
"properties": {
"name": "CyrusOne Carrollton Data Center",
"operator": "CyrusOne",
"capacity_mw": 100,
"status": "operational",
"year_opened": 2011,
"region": "ERCOT"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-97.7194, 30.2655] },
"properties": {
"name": "CyrusOne Austin Data Center",
"operator": "CyrusOne",
"capacity_mw": 60,
"status": "operational",
"year_opened": 2015,
"region": "ERCOT"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-84.3880, 33.7554] },
"properties": {
"name": "QTS Atlanta Metro Data Center",
"operator": "QTS",
"capacity_mw": 100,
"status": "operational",
"year_opened": 2013,
"region": "SERC"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-112.1161, 33.5605] },
"properties": {
"name": "CyrusOne Chandler Data Center",
"operator": "CyrusOne",
"capacity_mw": 80,
"status": "operational",
"year_opened": 2018,
"region": "SPP"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-77.4510, 38.9275] },
"properties": {
"name": "CoreSite Reston Data Center",
"operator": "CoreSite",
"capacity_mw": 50,
"status": "operational",
"year_opened": 2013,
"region": "PJM"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-104.8563, 39.5731] },
"properties": {
"name": "CoreSite Denver Data Center",
"operator": "CoreSite",
"capacity_mw": 40,
"status": "operational",
"year_opened": 2010,
"region": "SPP"
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-97.5028, 35.4810] },
"properties": {
"name": "Oracle Stargate Abilene Campus",
"operator": "Oracle",
"capacity_mw": 200,
"status": "under_construction",
"year_opened": 2025,
"region": "ERCOT"
}
}
]
}

345
data/grid-regions.geojson Normal file
View File

@ -0,0 +1,345 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-83.67, 41.73],
[-84.82, 41.76],
[-84.82, 39.10],
[-84.43, 38.45],
[-83.65, 38.63],
[-82.60, 38.17],
[-81.95, 37.54],
[-81.23, 37.27],
[-80.52, 37.48],
[-80.30, 37.10],
[-79.51, 36.54],
[-78.45, 35.69],
[-77.75, 36.00],
[-75.87, 36.55],
[-75.24, 37.77],
[-75.62, 38.46],
[-74.98, 38.93],
[-74.70, 39.30],
[-74.18, 39.62],
[-74.01, 40.07],
[-74.72, 40.15],
[-75.06, 39.99],
[-75.13, 39.88],
[-75.53, 39.84],
[-76.04, 39.72],
[-77.25, 39.32],
[-77.49, 39.10],
[-77.72, 39.32],
[-77.83, 39.64],
[-79.48, 39.72],
[-80.52, 40.64],
[-80.52, 41.98],
[-81.28, 42.21],
[-82.00, 41.96],
[-83.13, 41.96],
[-83.67, 41.73]
]
],
[
[
[-74.72, 40.15],
[-74.01, 40.07],
[-73.89, 40.57],
[-74.25, 40.53],
[-75.14, 40.68],
[-75.12, 41.85],
[-76.11, 42.00],
[-79.76, 42.27],
[-80.52, 41.98],
[-80.52, 40.64],
[-79.48, 39.72],
[-77.83, 39.64],
[-77.72, 39.32],
[-77.49, 39.10],
[-77.25, 39.32],
[-76.04, 39.72],
[-75.53, 39.84],
[-75.13, 39.88],
[-75.06, 39.99],
[-74.72, 40.15]
]
],
[
[
[-87.53, 41.76],
[-87.53, 39.35],
[-87.53, 38.23],
[-87.69, 37.79],
[-87.10, 37.79],
[-86.52, 36.64],
[-85.98, 36.63],
[-84.86, 36.63],
[-84.43, 38.45],
[-84.82, 39.10],
[-84.82, 41.76],
[-87.53, 41.76]
]
]
]
},
"properties": {
"name": "PJM Interconnection",
"code": "PJM",
"iso": "PJM"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-106.65, 31.75],
[-103.06, 31.97],
[-100.00, 31.00],
[-99.41, 27.84],
[-97.14, 25.97],
[-96.36, 28.14],
[-93.84, 29.71],
[-93.72, 31.08],
[-94.04, 33.55],
[-96.31, 33.90],
[-97.37, 33.97],
[-100.00, 34.56],
[-103.04, 32.00],
[-106.65, 31.75]
]
]
]
},
"properties": {
"name": "Electric Reliability Council of Texas",
"code": "ERCOT",
"iso": "ERCOT"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-124.41, 42.00],
[-123.23, 42.00],
[-120.00, 42.00],
[-120.00, 39.00],
[-117.63, 37.43],
[-116.09, 35.98],
[-114.63, 34.87],
[-114.63, 32.72],
[-117.12, 32.54],
[-118.60, 33.78],
[-120.63, 34.57],
[-121.89, 36.60],
[-122.39, 37.62],
[-122.47, 37.81],
[-123.03, 38.31],
[-123.73, 39.33],
[-124.41, 40.44],
[-124.41, 42.00]
]
]
]
},
"properties": {
"name": "California Independent System Operator",
"code": "CAISO",
"iso": "CAISO"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-79.76, 42.27],
[-79.76, 43.28],
[-78.89, 42.95],
[-76.80, 43.63],
[-76.18, 44.20],
[-75.32, 44.81],
[-74.87, 45.01],
[-73.34, 45.01],
[-73.34, 42.05],
[-73.73, 41.10],
[-74.25, 40.53],
[-73.89, 40.57],
[-74.01, 40.07],
[-73.74, 40.63],
[-72.76, 40.75],
[-71.85, 40.98],
[-73.73, 41.10],
[-73.34, 42.05],
[-79.76, 42.27]
]
]
]
},
"properties": {
"name": "New York Independent System Operator",
"code": "NYISO",
"iso": "NYISO"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-73.73, 41.10],
[-71.85, 40.98],
[-71.12, 41.49],
[-69.93, 41.67],
[-70.60, 41.78],
[-70.82, 42.67],
[-70.70, 43.07],
[-69.04, 43.98],
[-68.12, 44.38],
[-67.79, 44.55],
[-67.10, 45.14],
[-67.10, 47.27],
[-68.57, 47.29],
[-70.25, 46.25],
[-71.08, 45.30],
[-71.50, 45.01],
[-73.34, 45.01],
[-73.34, 42.05],
[-73.73, 41.10]
]
]
]
},
"properties": {
"name": "ISO New England",
"code": "ISONE",
"iso": "ISONE"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-97.23, 49.00],
[-95.15, 49.00],
[-95.15, 48.00],
[-89.49, 48.01],
[-84.72, 46.63],
[-83.59, 46.03],
[-84.11, 45.18],
[-85.61, 44.77],
[-86.46, 43.89],
[-86.27, 42.40],
[-86.80, 41.76],
[-87.53, 41.76],
[-87.53, 38.23],
[-87.69, 37.79],
[-88.07, 37.50],
[-88.47, 37.07],
[-89.10, 36.95],
[-90.18, 36.50],
[-94.62, 36.50],
[-94.62, 37.00],
[-95.07, 37.00],
[-95.78, 39.99],
[-96.00, 40.00],
[-96.45, 42.49],
[-96.63, 42.52],
[-96.44, 43.50],
[-96.45, 45.30],
[-96.56, 45.94],
[-97.23, 49.00]
]
],
[
[
[-89.10, 36.95],
[-89.70, 36.25],
[-89.67, 34.96],
[-90.31, 34.73],
[-90.58, 34.14],
[-91.15, 33.01],
[-91.17, 31.55],
[-91.65, 31.00],
[-93.53, 31.18],
[-93.72, 31.08],
[-93.84, 29.71],
[-93.84, 30.25],
[-94.04, 31.00],
[-94.04, 33.55],
[-94.48, 33.64],
[-94.43, 35.39],
[-94.62, 36.50],
[-90.18, 36.50],
[-89.10, 36.95]
]
]
]
},
"properties": {
"name": "Midcontinent Independent System Operator",
"code": "MISO",
"iso": "MISO"
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-104.05, 49.00],
[-97.23, 49.00],
[-96.56, 45.94],
[-96.45, 45.30],
[-96.44, 43.50],
[-96.63, 42.52],
[-96.45, 42.49],
[-96.00, 40.00],
[-95.78, 39.99],
[-95.07, 37.00],
[-94.62, 37.00],
[-94.62, 36.50],
[-94.43, 35.39],
[-94.48, 33.64],
[-96.31, 33.90],
[-100.00, 34.56],
[-103.00, 36.50],
[-103.00, 37.00],
[-104.05, 38.00],
[-104.05, 41.00],
[-104.05, 43.00],
[-104.05, 45.94],
[-104.05, 49.00]
]
]
]
},
"properties": {
"name": "Southwest Power Pool",
"code": "SPP",
"iso": "SPP"
}
}
]
}

14
docker-compose.yml Normal file
View File

@ -0,0 +1,14 @@
services:
db:
image: postgis/postgis:18-3.6
ports:
- "5433:5432"
environment:
POSTGRES_DB: energy_dashboard
POSTGRES_USER: energy
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql
volumes:
pgdata:

View File

@ -1,18 +1,18 @@
import eslint from '@eslint/js';
import nextPlugin from '@next/eslint-plugin-next';
import pluginQuery from '@tanstack/eslint-plugin-query';
import parser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
import prettierPlugin from 'eslint-plugin-prettier/recommended';
import pluginReact from 'eslint-plugin-react';
import reactCompiler from 'eslint-plugin-react-compiler';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import globals from 'globals';
import eslint from '@eslint/js';
import importPlugin from 'eslint-plugin-import';
import prettierPlugin from 'eslint-plugin-prettier/recommended';
import tseslint from 'typescript-eslint';
/** @type {import("eslint").Linter.Config} */
export default [
...eslint.configs.recommended,
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
prettierPlugin,
{
@ -149,8 +149,6 @@ export default [
'warn',
{ selector: "CallExpression[callee.name='unwrap']", message: 'Handle errors instead of unwrapping!' },
],
'meridian/no-direct-cache-tag': 'error',
'meridian/no-private-env-unless-server-only': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{ checksVoidReturn: { arguments: false, attributes: false } },
@ -160,14 +158,14 @@ export default [
},
},
{
files: ['app/**/{page,layout,not-found,default,loading,global-error,error}.tsx'],
files: ['src/app/**/{page,layout,not-found,default,loading,global-error,error}.tsx'],
rules: {
'import/no-default-export': 'off',
'@typescript-eslint/require-await': 'off',
},
},
{
files: ['app/**/routeType.ts'],
files: ['src/app/**/routeType.ts'],
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
@ -180,16 +178,13 @@ export default [
],
},
},
{
files: ['lib/cache.ts'],
rules: {
'meridian/no-direct-cache-tag': 'off',
},
},
{
files: ['next.config.ts'],
rules: { '@typescript-eslint/no-unsafe-type-assertion': 'off', 'import/no-default-export': 'off' },
},
{ ignores: ['next-env.d.ts', 'gen/**/*'] },
{
files: ['prisma.config.ts'],
rules: { 'import/no-default-export': 'off' },
},
{ ignores: ['next-env.d.ts', 'gen/**/*', 'src/generated/**'] },
];

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
typedRoutes: true,
};
export default nextConfig;

65
package.json Normal file
View File

@ -0,0 +1,65 @@
{
"name": "bonus4",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint . --max-warnings 0"
},
"devDependencies": {
"@eslint/js": "9",
"@next/eslint-plugin-next": "^16.1.6",
"@tanstack/eslint-plugin-query": "^5.91.4",
"@types/bun": "latest",
"@types/node": "^25.2.3",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/parser": "^8.55.0",
"eslint": "9",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^17.3.0",
"prettier": "^3.8.1",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-tailwindcss": "^0.7.2",
"typescript-eslint": "^8.55.0",
"zod-validation-error": "^5.0.0"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@tailwindcss/postcss": "^4.1.18",
"@vis.gl/react-google-maps": "^1.7.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.4",
"framer-motion": "^12.34.0",
"lucide-react": "^0.563.0",
"next": "^16.1.6",
"next-themes": "^0.4.6",
"postcss": "^8.5.6",
"prisma": "^7.3.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^3.7.0",
"sonner": "^2.0.7",
"superjson": "^2.2.6",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"tslib": "^2.8.1",
"tw-animate-css": "^1.4.0",
"zod": "^4.3.6"
},
"overrides": {
"zod-validation-error": "^5.0.0"
}
}

5
postcss.config.js Normal file
View File

@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};

13
prisma.config.ts Normal file
View File

@ -0,0 +1,13 @@
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: 'bun run prisma/seed.ts',
},
datasource: {
url: env('DATABASE_URL'),
},
});

View File

@ -0,0 +1,85 @@
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "postgis";
-- CreateTable
CREATE TABLE "grid_regions" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"code" TEXT NOT NULL,
"iso" TEXT NOT NULL,
"boundary" geography(MultiPolygon, 4326) NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "grid_regions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "datacenters" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"operator" TEXT NOT NULL,
"location" geography(Point, 4326) NOT NULL,
"capacity_mw" DOUBLE PRECISION NOT NULL,
"status" TEXT NOT NULL,
"year_opened" INTEGER NOT NULL,
"region_id" UUID NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "datacenters_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "electricity_prices" (
"id" UUID NOT NULL,
"region_id" UUID NOT NULL,
"price_mwh" DOUBLE PRECISION NOT NULL,
"demand_mw" DOUBLE PRECISION NOT NULL,
"timestamp" TIMESTAMPTZ NOT NULL,
"source" TEXT NOT NULL,
CONSTRAINT "electricity_prices_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "commodity_prices" (
"id" UUID NOT NULL,
"commodity" TEXT NOT NULL,
"price" DOUBLE PRECISION NOT NULL,
"unit" TEXT NOT NULL,
"timestamp" TIMESTAMPTZ NOT NULL,
"source" TEXT NOT NULL,
CONSTRAINT "commodity_prices_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "generation_mix" (
"id" UUID NOT NULL,
"region_id" UUID NOT NULL,
"fuel_type" TEXT NOT NULL,
"generation_mw" DOUBLE PRECISION NOT NULL,
"timestamp" TIMESTAMPTZ NOT NULL,
CONSTRAINT "generation_mix_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "grid_regions_code_key" ON "grid_regions"("code");
-- CreateIndex
CREATE INDEX "electricity_prices_region_id_timestamp_idx" ON "electricity_prices"("region_id", "timestamp");
-- CreateIndex
CREATE INDEX "commodity_prices_commodity_timestamp_idx" ON "commodity_prices"("commodity", "timestamp");
-- CreateIndex
CREATE INDEX "generation_mix_region_id_timestamp_idx" ON "generation_mix"("region_id", "timestamp");
-- AddForeignKey
ALTER TABLE "datacenters" ADD CONSTRAINT "datacenters_region_id_fkey" FOREIGN KEY ("region_id") REFERENCES "grid_regions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "electricity_prices" ADD CONSTRAINT "electricity_prices_region_id_fkey" FOREIGN KEY ("region_id") REFERENCES "grid_regions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "generation_mix" ADD CONSTRAINT "generation_mix_region_id_fkey" FOREIGN KEY ("region_id") REFERENCES "grid_regions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

75
prisma/schema.prisma Normal file
View File

@ -0,0 +1,75 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
previewFeatures = ["typedSql"]
}
datasource db {
provider = "postgresql"
}
model GridRegion {
id String @id @default(uuid()) @db.Uuid
name String
code String @unique
iso String
boundary Unsupported("geography(MultiPolygon, 4326)")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
datacenters Datacenter[]
electricityPrices ElectricityPrice[]
generationMixes GenerationMix[]
@@map("grid_regions")
}
model Datacenter {
id String @id @default(uuid()) @db.Uuid
name String
operator String
location Unsupported("geography(Point, 4326)")
capacityMw Float @map("capacity_mw")
status String
yearOpened Int @map("year_opened")
regionId String @map("region_id") @db.Uuid
region GridRegion @relation(fields: [regionId], references: [id])
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
@@map("datacenters")
}
model ElectricityPrice {
id String @id @default(uuid()) @db.Uuid
regionId String @map("region_id") @db.Uuid
region GridRegion @relation(fields: [regionId], references: [id])
priceMwh Float @map("price_mwh")
demandMw Float @map("demand_mw")
timestamp DateTime @db.Timestamptz
source String
@@index([regionId, timestamp])
@@map("electricity_prices")
}
model CommodityPrice {
id String @id @default(uuid()) @db.Uuid
commodity String
price Float
unit String
timestamp DateTime @db.Timestamptz
source String
@@index([commodity, timestamp])
@@map("commodity_prices")
}
model GenerationMix {
id String @id @default(uuid()) @db.Uuid
regionId String @map("region_id") @db.Uuid
region GridRegion @relation(fields: [regionId], references: [id])
fuelType String @map("fuel_type")
generationMw Float @map("generation_mw")
timestamp DateTime @db.Timestamptz
@@index([regionId, timestamp])
@@map("generation_mix")
}

229
prisma/seed.ts Normal file
View File

@ -0,0 +1,229 @@
import { PrismaPg } from '@prisma/adapter-pg';
import { randomUUID } from 'crypto';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { z } from 'zod/v4';
import { PrismaClient } from '../src/generated/prisma/client.js';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
const PointGeometrySchema = z.object({
type: z.literal('Point'),
coordinates: z.tuple([z.number(), z.number()]),
});
const MultiPolygonGeometrySchema = z.object({
type: z.literal('MultiPolygon'),
coordinates: z.array(z.array(z.array(z.tuple([z.number(), z.number()])))),
});
const RegionPropertiesSchema = z.object({
name: z.string(),
code: z.string(),
iso: z.string(),
});
const DatacenterPropertiesSchema = z.object({
name: z.string(),
operator: z.string(),
capacity_mw: z.number(),
status: z.string(),
year_opened: z.number(),
region: z.string(),
});
const RegionFeatureSchema = z.object({
type: z.literal('Feature'),
geometry: MultiPolygonGeometrySchema,
properties: RegionPropertiesSchema,
});
const DatacenterFeatureSchema = z.object({
type: z.literal('Feature'),
geometry: PointGeometrySchema,
properties: DatacenterPropertiesSchema,
});
const RegionCollectionSchema = z.object({
type: z.literal('FeatureCollection'),
features: z.array(RegionFeatureSchema),
});
const DatacenterCollectionSchema = z.object({
type: z.literal('FeatureCollection'),
features: z.array(DatacenterFeatureSchema),
});
const AIMilestoneSchema = z.object({
date: z.string(),
title: z.string(),
description: z.string(),
category: z.string(),
});
function readAndParse<T>(relativePath: string, schema: z.ZodType<T>): T {
const fullPath = resolve(import.meta.dirname, '..', relativePath);
const raw: unknown = JSON.parse(readFileSync(fullPath, 'utf-8'));
return schema.parse(raw);
}
async function seedGridRegions() {
console.log('Seeding grid regions...');
const geojson = readAndParse('data/grid-regions.geojson', RegionCollectionSchema);
// Delete existing data (order matters for foreign keys)
await prisma.$executeRawUnsafe('DELETE FROM datacenters');
await prisma.$executeRawUnsafe('DELETE FROM electricity_prices');
await prisma.$executeRawUnsafe('DELETE FROM generation_mix');
await prisma.$executeRawUnsafe('DELETE FROM grid_regions');
for (const feature of geojson.features) {
const id = randomUUID();
const geojsonStr = JSON.stringify(feature.geometry);
await prisma.$executeRawUnsafe(
`INSERT INTO grid_regions (id, name, code, iso, boundary, created_at)
VALUES ($1::uuid, $2, $3, $4, ST_GeomFromGeoJSON($5)::geography, NOW())`,
id,
feature.properties.name,
feature.properties.code,
feature.properties.iso,
geojsonStr,
);
console.log(` Inserted region: ${feature.properties.code}`);
}
}
async function seedDatacenters() {
console.log('Seeding datacenters...');
const geojson = readAndParse('data/datacenters.geojson', DatacenterCollectionSchema);
// Get all region codes from DB
const regions = await prisma.$queryRawUnsafe<Array<{ id: string; code: string }>>(
'SELECT id, code FROM grid_regions',
);
const regionMap = new Map(regions.map(r => [r.code, r.id]));
let inserted = 0;
let skipped = 0;
for (const feature of geojson.features) {
const props = feature.properties;
const regionId = regionMap.get(props.region);
if (!regionId) {
// Try to find the region by spatial containment
const [lng, lat] = feature.geometry.coordinates;
const spatialMatch = await prisma.$queryRawUnsafe<Array<{ id: string; code: string }>>(
`SELECT id, code FROM grid_regions
WHERE ST_Contains(boundary::geometry, ST_SetSRID(ST_MakePoint($1, $2), 4326))
LIMIT 1`,
lng,
lat,
);
if (spatialMatch.length > 0) {
const match = spatialMatch[0]!;
const id = randomUUID();
const geojsonStr = JSON.stringify(feature.geometry);
await prisma.$executeRawUnsafe(
`INSERT INTO datacenters (id, name, operator, location, capacity_mw, status, year_opened, region_id, created_at)
VALUES ($1::uuid, $2, $3, ST_GeomFromGeoJSON($4)::geography, $5, $6, $7, $8::uuid, NOW())`,
id,
props.name,
props.operator,
geojsonStr,
props.capacity_mw,
props.status,
props.year_opened,
match.id,
);
console.log(` Inserted DC: ${props.name} (spatial match -> ${match.code})`);
inserted++;
} else {
console.log(` Skipped DC: ${props.name} (no matching region for "${props.region}")`);
skipped++;
}
continue;
}
const id = randomUUID();
const geojsonStr = JSON.stringify(feature.geometry);
await prisma.$executeRawUnsafe(
`INSERT INTO datacenters (id, name, operator, location, capacity_mw, status, year_opened, region_id, created_at)
VALUES ($1::uuid, $2, $3, ST_GeomFromGeoJSON($4)::geography, $5, $6, $7, $8::uuid, NOW())`,
id,
props.name,
props.operator,
geojsonStr,
props.capacity_mw,
props.status,
props.year_opened,
regionId,
);
console.log(` Inserted DC: ${props.name} (${props.region})`);
inserted++;
}
console.log(` Total: ${inserted.toString()} inserted, ${skipped.toString()} skipped`);
}
function validateAIMilestones() {
console.log('Validating AI milestones...');
const milestones = readAndParse('data/ai-milestones.json', z.array(AIMilestoneSchema));
console.log(` Valid JSON with ${milestones.length.toString()} milestones`);
for (const m of milestones) {
if (isNaN(Date.parse(m.date))) {
throw new Error(`Invalid date in milestone: ${m.date}`);
}
}
console.log(' All milestones valid');
}
async function main() {
console.log('Starting seed...\n');
await seedGridRegions();
console.log('');
await seedDatacenters();
console.log('');
validateAIMilestones();
// Print summary
console.log('\n--- Seed Summary ---');
const regionCount = await prisma.$queryRawUnsafe<Array<{ count: bigint }>>(
'SELECT count(*) as count FROM grid_regions',
);
const dcCount = await prisma.$queryRawUnsafe<Array<{ count: bigint }>>('SELECT count(*) as count FROM datacenters');
console.log(`Grid regions: ${regionCount[0]!.count.toString()}`);
console.log(`Datacenters: ${dcCount[0]!.count.toString()}`);
// Show sample spatial data
const sample = await prisma.$queryRawUnsafe<Array<{ name: string; location_text: string }>>(
'SELECT name, ST_AsText(location) as location_text FROM datacenters LIMIT 3',
);
console.log('\nSample datacenter locations:');
for (const s of sample) {
console.log(` ${s.name}: ${s.location_text}`);
}
console.log('\nSeed complete!');
}
main()
.catch((e: unknown) => {
console.error('Seed failed:', e);
process.exit(1);
})
.finally(() => {
void prisma.$disconnect();
});

View File

@ -0,0 +1,8 @@
-- @param {String} $1:regionCode
SELECT
d.id, d.name, d.operator, d.capacity_mw, d.status, d.year_opened,
ST_AsGeoJSON(d.location)::TEXT as location_geojson
FROM datacenters d
JOIN grid_regions r ON d.region_id = r.id
WHERE r.code = $1
ORDER BY d.capacity_mw DESC

View File

@ -0,0 +1,10 @@
-- @param {Float} $1:lat
-- @param {Float} $2:lng
-- @param {Float} $3:radiusKm
SELECT
d.id, d.name, d.operator, d.capacity_mw,
ST_AsGeoJSON(d.location)::TEXT as location_geojson,
ST_Distance(d.location, ST_MakePoint($2, $1)::geography) / 1000 as distance_km
FROM datacenters d
WHERE ST_DWithin(d.location, ST_MakePoint($2, $1)::geography, $3 * 1000)
ORDER BY distance_km

View File

@ -0,0 +1,15 @@
-- @param {DateTime} $1:startDate
-- @param {DateTime} $2:endDate
SELECT
r.code as region_code, r.name as region_name,
date_trunc('day', ep.timestamp) as day,
AVG(ep.demand_mw) as avg_demand,
MAX(ep.demand_mw) as peak_demand,
COUNT(DISTINCT d.id)::INT as datacenter_count,
COALESCE(SUM(DISTINCT d.capacity_mw), 0) as total_dc_capacity_mw
FROM grid_regions r
LEFT JOIN electricity_prices ep ON ep.region_id = r.id
AND ep.timestamp BETWEEN $1 AND $2
LEFT JOIN datacenters d ON d.region_id = r.id
GROUP BY r.id, r.code, r.name, date_trunc('day', ep.timestamp)
ORDER BY r.code, day

View File

@ -0,0 +1,11 @@
-- @param {String} $1:regionCode
-- @param {DateTime} $2:startDate
-- @param {DateTime} $3:endDate
SELECT
gm.fuel_type, gm.timestamp, gm.generation_mw,
r.code as region_code, r.name as region_name
FROM generation_mix gm
JOIN grid_regions r ON gm.region_id = r.id
WHERE r.code = $1
AND gm.timestamp BETWEEN $2 AND $3
ORDER BY gm.timestamp ASC, gm.fuel_type

View File

@ -0,0 +1,6 @@
SELECT DISTINCT ON (ep.region_id)
ep.id, ep.region_id, ep.price_mwh, ep.demand_mw, ep.timestamp, ep.source,
r.code as region_code, r.name as region_name
FROM electricity_prices ep
JOIN grid_regions r ON ep.region_id = r.id
ORDER BY ep.region_id, ep.timestamp DESC

View File

@ -0,0 +1,11 @@
-- @param {String} $1:regionCode
-- @param {DateTime} $2:startDate
-- @param {DateTime} $3:endDate
SELECT
ep.timestamp, ep.price_mwh, ep.demand_mw,
r.code as region_code, r.name as region_name
FROM electricity_prices ep
JOIN grid_regions r ON ep.region_id = r.id
WHERE r.code = $1
AND ep.timestamp BETWEEN $2 AND $3
ORDER BY ep.timestamp ASC

View File

@ -0,0 +1,13 @@
SELECT
r.code, r.name,
ST_AsGeoJSON(r.boundary)::TEXT as boundary_geojson,
AVG(ep.price_mwh) as avg_price,
MAX(ep.price_mwh) as max_price,
AVG(ep.demand_mw) as avg_demand,
COUNT(DISTINCT d.id)::INT as datacenter_count,
COALESCE(SUM(d.capacity_mw), 0) as total_dc_capacity_mw
FROM grid_regions r
LEFT JOIN electricity_prices ep ON ep.region_id = r.id
AND ep.timestamp > NOW() - INTERVAL '24 hours'
LEFT JOIN datacenters d ON d.region_id = r.id
GROUP BY r.id, r.code, r.name, r.boundary

123
src/app/globals.css Normal file
View File

@ -0,0 +1,123 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

23
src/app/layout.tsx Normal file
View File

@ -0,0 +1,23 @@
import type { Metadata } from 'next';
import { ThemeProvider } from 'next-themes';
import { Toaster } from 'sonner';
import './globals.css';
export const metadata: Metadata = {
title: 'Energy & AI Dashboard',
description:
'Interactive dashboard visualizing how AI datacenter buildout is driving regional electricity demand and energy prices',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="min-h-screen antialiased">
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false} disableTransitionOnChange>
{children}
<Toaster theme="dark" richColors position="bottom-right" />
</ThemeProvider>
</body>
</html>
);
}

8
src/app/page.tsx Normal file
View File

@ -0,0 +1,8 @@
export default function DashboardHome() {
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold tracking-tight">Energy &amp; AI Dashboard</h1>
<p className="mt-4 text-muted-foreground">Phase 1 scaffold complete. Dashboard coming soon.</p>
</main>
);
}

16
src/lib/db.ts Normal file
View File

@ -0,0 +1,16 @@
import { PrismaClient } from '@/generated/prisma/client.js';
import { PrismaPg } from '@prisma/adapter-pg';
const globalPrismaKey = Symbol.for('prisma');
type GlobalWithPrisma = typeof globalThis & { [globalPrismaKey]?: PrismaClient };
function createPrismaClient() {
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
return new PrismaClient({ adapter });
}
const g = globalThis as GlobalWithPrisma;
export const prisma = g[globalPrismaKey] ?? createPrismaClient();
if (process.env.NODE_ENV !== 'production') g[globalPrismaKey] = prisma;

11
src/lib/superjson.ts Normal file
View File

@ -0,0 +1,11 @@
import SuperJSON from 'superjson';
export function serialize<T>(data: T) {
return SuperJSON.serialize(data);
}
export function deserialize<T>(data: ReturnType<typeof SuperJSON.serialize>): T {
return SuperJSON.deserialize<T>(data);
}
export { SuperJSON };

27
src/lib/utils.ts Normal file
View File

@ -0,0 +1,27 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const REGION_TIMEZONES: Record<string, string> = {
ERCOT: 'America/Chicago',
PJM: 'America/New_York',
CAISO: 'America/Los_Angeles',
NYISO: 'America/New_York',
ISONE: 'America/New_York',
MISO: 'America/Chicago',
SPP: 'America/Chicago',
};
export function formatMarketTime(utcDate: Date, regionCode: string): string {
const timezone = REGION_TIMEZONES[regionCode] ?? 'America/New_York';
return utcDate.toLocaleString('en-US', {
timeZone: timezone,
hour: 'numeric',
minute: '2-digit',
hour12: true,
timeZoneName: 'short',
});
}

View File

@ -24,13 +24,41 @@
"preserveConstEnums": true,
"sourceMap": true,
"isolatedModules": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "esnext",
"plugins": [{ "name": "next" }],
"plugins": [
{
"name": "next"
}
],
"allowJs": true,
"jsx": "preserve",
"noEmit": true
"jsx": "react-jsx",
"noEmit": true,
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"next.config.ts",
"prisma.config.ts",
"prisma/seed.ts",
"src/**/*.ts",
"src/**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules",
".next",
"dist"
]
}