diff --git a/package-lock.json b/package-lock.json
index 0040908..b19614f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "rest-express",
+ "name": "gridtilt",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "rest-express",
+ "name": "gridtilt",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
@@ -15,7 +15,6 @@
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-toast": "^1.2.7",
- "@radix-ui/react-toggle": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@resvg/resvg-js": "^2.6.2",
"@tanstack/react-query": "^5.60.5",
@@ -31,7 +30,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
- "date-fns": "^3.6.0",
"express": "^5.0.1",
"express-rate-limit": "^7.4.1",
"helmet": "^8.0.0",
@@ -58,7 +56,6 @@
"@replit/vite-plugin-dev-banner": "^0.1.1",
"@replit/vite-plugin-runtime-error-modal": "^0.0.3",
"@tailwindcss/typography": "^0.5.15",
- "@tailwindcss/vite": "^4.1.18",
"@types/express": "^5.0.0",
"@types/node": "20.19.27",
"@types/react": "^18.3.11",
@@ -1421,30 +1418,6 @@
}
}
},
- "node_modules/@radix-ui/react-toggle": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.3.tgz",
- "integrity": "sha512-Za5HHd9nvsiZ2t3EI/dVd4Bm/JydK+D22uHKk46fPtvuPxVCJBUo5mQybN+g5sZe35y7I6GDTTfdkVv5SnxlFg==",
- "dependencies": {
- "@radix-ui/primitive": "1.1.2",
- "@radix-ui/react-primitive": "2.0.3",
- "@radix-ui/react-use-controllable-state": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.0.tgz",
@@ -2273,286 +2246,6 @@
"node": ">= 8.0.0"
}
},
- "node_modules/@tailwindcss/node": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
- "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/remapping": "^2.3.4",
- "enhanced-resolve": "^5.18.3",
- "jiti": "^2.6.1",
- "lightningcss": "1.30.2",
- "magic-string": "^0.30.21",
- "source-map-js": "^1.2.1",
- "tailwindcss": "4.1.18"
- }
- },
- "node_modules/@tailwindcss/node/node_modules/jiti": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
- "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
- "node_modules/@tailwindcss/node/node_modules/tailwindcss": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
- "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tailwindcss/oxide": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
- "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 10"
- },
- "optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.1.18",
- "@tailwindcss/oxide-darwin-arm64": "4.1.18",
- "@tailwindcss/oxide-darwin-x64": "4.1.18",
- "@tailwindcss/oxide-freebsd-x64": "4.1.18",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
- "@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
- "@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
- "@tailwindcss/oxide-linux-x64-musl": "4.1.18",
- "@tailwindcss/oxide-wasm32-wasi": "4.1.18",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
- "@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
- }
- },
- "node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
- "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
- "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
- "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
- "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
- "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
- "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
- "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
- "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
- "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
- "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
- "bundleDependencies": [
- "@napi-rs/wasm-runtime",
- "@emnapi/core",
- "@emnapi/runtime",
- "@tybys/wasm-util",
- "@emnapi/wasi-threads",
- "tslib"
- ],
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1",
- "@emnapi/wasi-threads": "^1.1.0",
- "@napi-rs/wasm-runtime": "^1.1.0",
- "@tybys/wasm-util": "^0.10.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
- "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
- "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@tailwindcss/typography": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
@@ -2583,28 +2276,6 @@
"node": ">=4"
}
},
- "node_modules/@tailwindcss/vite": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz",
- "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@tailwindcss/node": "4.1.18",
- "@tailwindcss/oxide": "4.1.18",
- "tailwindcss": "4.1.18"
- },
- "peerDependencies": {
- "vite": "^5.2.0 || ^6 || ^7"
- }
- },
- "node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
- "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@tanstack/query-core": {
"version": "5.60.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.5.tgz",
@@ -4362,15 +4033,6 @@
"node": ">=12"
}
},
- "node_modules/date-fns": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
- "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/kossnocorp"
- }
- },
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@@ -4412,15 +4074,6 @@
"node": ">= 0.8"
}
},
- "node_modules/detect-libc": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
- "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@@ -4504,20 +4157,6 @@
"node": ">= 0.8"
}
},
- "node_modules/enhanced-resolve": {
- "version": "5.18.4",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
- "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
@@ -5050,13 +4689,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -5332,273 +4964,6 @@
"leaflet": "^1.3.1"
}
},
- "node_modules/lightningcss": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
- "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.30.2",
- "lightningcss-darwin-arm64": "1.30.2",
- "lightningcss-darwin-x64": "1.30.2",
- "lightningcss-freebsd-x64": "1.30.2",
- "lightningcss-linux-arm-gnueabihf": "1.30.2",
- "lightningcss-linux-arm64-gnu": "1.30.2",
- "lightningcss-linux-arm64-musl": "1.30.2",
- "lightningcss-linux-x64-gnu": "1.30.2",
- "lightningcss-linux-x64-musl": "1.30.2",
- "lightningcss-win32-arm64-msvc": "1.30.2",
- "lightningcss-win32-x64-msvc": "1.30.2"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
- "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
- "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
- "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
- "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
- "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
- "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
- "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
- "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
- "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
- "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
- "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -7178,20 +6543,6 @@
"tailwindcss": ">=3.0.0 || insiders"
}
},
- "node_modules/tapable": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
- "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
diff --git a/package.json b/package.json
index 2decf29..2731d2e 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "rest-express",
+ "name": "gridtilt",
"version": "1.0.0",
"type": "module",
"license": "MIT",
@@ -18,7 +18,6 @@
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-toast": "^1.2.7",
- "@radix-ui/react-toggle": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@resvg/resvg-js": "^2.6.2",
"@tanstack/react-query": "^5.60.5",
@@ -34,7 +33,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
- "date-fns": "^3.6.0",
"express": "^5.0.1",
"express-rate-limit": "^7.4.1",
"helmet": "^8.0.0",
@@ -61,7 +59,6 @@
"@replit/vite-plugin-dev-banner": "^0.1.1",
"@replit/vite-plugin-runtime-error-modal": "^0.0.3",
"@tailwindcss/typography": "^0.5.15",
- "@tailwindcss/vite": "^4.1.18",
"@types/express": "^5.0.0",
"@types/node": "20.19.27",
"@types/react": "^18.3.11",
diff --git a/replit.md b/replit.md
index e90fb70..63bd98f 100644
--- a/replit.md
+++ b/replit.md
@@ -1,49 +1,32 @@
-# GridTilt | AI Infrastructure & Power Economy Dashboard
-
-## Overview
-GridTilt is a full-stack web application that visualizes the economic interplay between AI compute demand, power consumption, and financial markets. It provides a comprehensive dashboard for understanding and analyzing the AI infrastructure and power economy. The project aims to offer a unique perspective on how advancements in AI drive energy demands and influence market dynamics, with a vision to become the leading platform for tracking and analyzing the AI power sector.
-
-## User Preferences
-- No em dashes anywhere in the codebase.
-- Text style: short, direct sentences. No compound AI-sounding phrasing.
-- Dark mode only, with a warm charcoal terminal aesthetic and orange branding.
-
-## System Architecture
-
-### Frontend
-The frontend is built with React and TypeScript using Vite. It leverages `wouter` for routing, `TanStack Query v5` for data fetching, and `recharts` and `react-simple-maps` for data visualization. UI components are sourced from `shadcn/ui`, and styling is managed with `TailwindCSS` using a custom dark-mode-only theme.
-
-### Backend
-The backend is a Node.js and Express application. It fetches market data using `yahoo-finance2` and aggregates news from RSS feeds (Utility Dive, Data Center Dynamics, World Nuclear News, Power Engineering) with a fallback to `NewsData.io` if an API key is provided, otherwise utilizing static JSON. All API routes are prefixed with `/api`. The system includes fallbacks to static data when external APIs are unavailable.
-
-### Key Features
-- **TiltOverview**: Dashboard with KPIs, Thesis Health, Top Movers, Sector Pulse, Catalyst Calendar, X feed, and a demand chart.
-- **TheStack**: An 8-layer sector breakdown with live stock data, timeframe toggles, and sort controls.
-- **PowerMap**: An interactive Leaflet map with CartoDB Dark Matter tiles displaying 48 data center locations with glowing animated pins, frosted-glass tooltips, floating stats overlay, collapsible filter panel, and a Grid Stress mode.
-- **TheTrade**: A Thesis Calculator offering preset and custom scenarios for infrastructure buildout, capex, and LPT outputs.
-- **PortfolioOverlay**: Provides AI Power Exposure scoring and a radar chart for portfolios.
-- **CatalystTracker**: Monthly calendar grid with colored event dots, upcoming earnings timeline with stage-colored nodes, thesis catalyst cards with category badges. Merged API (`/api/catalysts/all`) combines live Yahoo Finance earnings dates (for 8 curated large-cap tickers) with manual thesis catalysts. Seed dates serve as fallback if Yahoo is unavailable. Dashboard "Next 5 Catalysts" widget on TiltOverview.
-- **Stock, Sector, Region, Operator Pages**: Dedicated pages for detailed analysis of individual stocks, sectors, grid regions, and hyperscaler operators.
-- **Blog**: Features analysis articles on AI infrastructure and power economy topics.
-- **SupplyChain (V3)**: Org-chart tree layout (Root > 5 Systems > Sub-system icons) that fits in one viewport on desktop. CSS Grid + SVG connector lines (no React Flow). 20 sub-systems across 5 stages with data config in `supply-chain-config.ts`. Click sub-system to expand inline detail panel with description, key metrics, and company stock cards. Click system node for summary panel. Dimming/active states, staggered entrance animations. Responsive: 5-across desktop, wrapped tablet, vertical accordion mobile with 3-col subsystem grid.
-- **Subscribe / Email Capture**: Email newsletter signup at /subscribe, inline capture at bottom of TiltOverview, scroll-triggered banner. Backend stores subscribers in JSON file (server/data/subscribers.json). Newsletter send via Resend (needs RESEND_API_KEY). Unsubscribe tokens use HMAC-SHA256 with SESSION_SECRET.
-
-### Visual Design
-The application features a dark charcoal terminal aesthetic with orange as the primary brand color for UI chrome, badges, and KPIs. Blue (`#1E90FF`) is strictly reserved for chart data series. The UI emphasizes short, direct sentences and avoids complex phrasing. Card radius is set to 0.35rem for a terminal-like appearance.
-
-### Global UX
-Includes a scrolling news ticker, keyboard shortcuts (`?` for modal, `G+1-6` for navigation), dynamic KPI cards with sparklines, and a shareable portfolio scoring feature. The `PowerMap` offers detailed filtering and a "Grid Stress" view. The `Thesis Calculator` provides scenario analysis with buildout charts and company rankings.
-
-## External Dependencies
-- **yahoo-finance2**: For fetching real-time market data.
-- **NewsData.io**: (Optional, with API key) For live news feeds.
-- **Recharts**: For charting and data visualization.
-- **leaflet** and **react-leaflet**: For interactive tile-based maps (CartoDB Dark Matter).
-- **shadcn/ui**: UI component library.
-- **TailwindCSS**: For styling and theming.
-- **wouter**: For client-side routing.
-- **TanStack Query v5**: For data fetching, caching, and state management.
-- **Node.js**: Backend runtime environment.
-- **Express**: Web application framework for the backend.
-- **satori** and **@resvg/resvg-js**: For dynamic OG image generation.
-- **RSS feeds**: Utility Dive, Data Center Dynamics, World Nuclear News, Power Engineering for news aggregation.
\ No newline at end of file
+# GridTilt
+
+Research dashboard for the AI power economy: equities, infrastructure, and power data. Live at gridtilt.com.
+
+**See CLAUDE.md for working conventions, module map, data custody, and do-not-touch zones. When this file and CLAUDE.md disagree, CLAUDE.md wins; when CLAUDE.md and the code disagree, the code wins.**
+
+## Stack
+
+- Client: React 18 + TypeScript + Vite, wouter routing, TanStack Query v5, Tailwind + house-modified shadcn/ui, Recharts / visx / d3 / react-leaflet. Dark mode only.
+- Server: Express 5, one process (Vite middleware in dev, static from `dist/public` in prod), port 5000.
+- Persistence: JSON files in `server/data/` and `content/blog/`. No database, no ORM, no `shared/` directory.
+- Not Next.js. No redux or app-level context.
+
+## Commands
+
+```bash
+npm run dev # tsx server with Vite middleware, port 5000
+npm run check # tsc typecheck
+npm test # node:test suites (server + client lib)
+npm run build # vite client + esbuild server -> dist/
+npm start # serve the built bundle
+```
+
+Required env: `UNSUB_TOKEN_SECRET`, `ADMIN_API_KEY` (see `.env.example`).
+
+## Deploy gotcha
+
+Replit autoscale. A push to GitHub does NOT deploy, and autoscale does not auto-pull. Merge, then Deployments -> Redeploy, manually. After redeploy, sanity-check `/api/stack`, `/api/kpis`, `/api/clusters/metrics`. Machine-written JSON in `server/data/` sits on ephemeral disk; a redeploy can lose it.
+
+## Style
+
+Plain, direct copy. No em dashes. Data honesty: serve real observations, flag estimates, degrade with explicit errors, never fabricate numbers.
diff --git a/script/build.ts b/script/build.ts
index e561021..07c967d 100644
--- a/script/build.ts
+++ b/script/build.ts
@@ -5,7 +5,6 @@ import { rm, readFile } from "fs/promises";
// server deps to bundle to reduce openat(2) syscalls
// which helps cold start times
const allowlist = [
- "date-fns",
"express",
"rss-parser",
"zod",
diff --git a/server/__tests__/auth-boundary.test.ts b/server/__tests__/auth-boundary.test.ts
new file mode 100644
index 0000000..41a97f8
--- /dev/null
+++ b/server/__tests__/auth-boundary.test.ts
@@ -0,0 +1,138 @@
+import { test, after } from "node:test";
+import assert from "node:assert/strict";
+import express from "express";
+import { createServer } from "node:http";
+import type { AddressInfo } from "node:net";
+
+// Must be set before importing routes: registerRoutes throws at call time if
+// UNSUB_TOKEN_SECRET is unset, and requireAdmin returns 503 (not 401) when
+// ADMIN_API_KEY is unset.
+process.env.UNSUB_TOKEN_SECRET ||= "test-secret-for-auth-test";
+process.env.ADMIN_API_KEY ||= "test-admin-key";
+process.env.NODE_ENV = "test";
+const ADMIN_KEY = process.env.ADMIN_API_KEY;
+
+// Keep every test offline: stub Yahoo so no route reaches the network.
+interface YahooLike {
+ quote: (...args: unknown[]) => Promise;
+ chart: (...args: unknown[]) => Promise;
+ search?: (...args: unknown[]) => Promise;
+}
+const yahooModule = await import("yahoo-finance2");
+const YahooFinanceClass = (yahooModule as unknown as { default: new () => YahooLike }).default;
+const proto = YahooFinanceClass.prototype as YahooLike;
+const originalQuote = proto.quote;
+const originalChart = proto.chart;
+proto.quote = () => Promise.reject(new Error("offline"));
+proto.chart = () => Promise.reject(new Error("offline"));
+after(() => {
+ proto.quote = originalQuote;
+ proto.chart = originalChart;
+});
+
+const { registerRoutes } = await import("../routes");
+
+async function startTestServer(): Promise<{ url: string; close: () => Promise }> {
+ const app = express();
+ app.use(express.json());
+ const httpServer = createServer(app);
+ await registerRoutes(httpServer, app);
+ await new Promise((resolve) => httpServer.listen(0, "127.0.0.1", resolve));
+ const { port } = httpServer.address() as AddressInfo;
+ return {
+ url: `http://127.0.0.1:${port}`,
+ close: () => new Promise((resolve) => httpServer.close(() => resolve())),
+ };
+}
+
+// Every admin/mutating route plus the two that used to leak. requireAdmin runs
+// before any body handling, so empty bodies are fine for the no-key case.
+const PROTECTED_ROUTES: Array<[string, string]> = [
+ ["GET", "/api/admin/subscribers"],
+ ["GET", "/api/newsletter/preview"], // SEC-1: was public, leaked subscriber count
+ ["POST", "/api/social/generate"], // SEC-2: was public, burned Yahoo quota
+ ["DELETE", "/api/admin/subscribers/x@y.com"],
+ ["POST", "/api/newsletter/send"],
+ ["POST", "/api/admin/post-now"],
+ ["POST", "/api/admin/cron/daily-tweet"],
+ ["GET", "/api/admin/social-log"],
+ ["DELETE", "/api/admin/tweet/123"],
+ ["POST", "/api/admin/datacenters"],
+ ["DELETE", "/api/admin/datacenters/1"],
+ ["GET", "/api/admin/datacenters/pending"],
+ ["POST", "/api/admin/datacenters/ingest"],
+ ["POST", "/api/admin/datacenters/pending/1/approve"],
+ ["DELETE", "/api/admin/datacenters/pending/1"],
+ ["GET", "/api/export/daily"],
+ ["POST", "/api/admin/add-backlog-project"],
+ ["DELETE", "/api/admin/backlog-project/x"],
+ ["GET", "/api/admin/backlog-auto-updates"],
+ ["POST", "/api/admin/scan-news-now"],
+ ["POST", "/api/admin/update-backlog-headlines"],
+];
+
+test("admin auth boundary: the correct key is accepted, a missing key never authorizes", async () => {
+ const { url, close } = await startTestServer();
+ try {
+ // 1. Happy path FIRST. Successful (2xx) requests are skipped by the
+ // admin failure limiter, so they don't consume its budget.
+ const okSubs = await fetch(`${url}/api/admin/subscribers`, {
+ headers: { "x-admin-key": ADMIN_KEY },
+ });
+ assert.equal(okSubs.status, 200, "valid key should reach GET /api/admin/subscribers");
+ await okSubs.text();
+
+ const okLog = await fetch(`${url}/api/admin/social-log`, {
+ headers: { "x-admin-key": ADMIN_KEY },
+ });
+ assert.equal(okLog.status, 200, "valid key should reach GET /api/admin/social-log");
+ await okLog.text();
+
+ // 2. The three most important no-key cases come first, while the failure
+ // limiter (5/min) still lets them through to the handler → exact 401.
+ for (const path of ["/api/admin/subscribers", "/api/newsletter/preview"]) {
+ const res = await fetch(`${url}${path}`);
+ assert.equal(res.status, 401, `${path} without a key must be 401, got ${res.status}`);
+ const body = (await res.json()) as { error?: string };
+ assert.equal(body.error, "Unauthorized", `${path} should report Unauthorized`);
+ }
+ const gen = await fetch(`${url}/api/social/generate`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: "{}",
+ });
+ assert.equal(gen.status, 401, `POST /api/social/generate without a key must be 401, got ${gen.status}`);
+ await gen.text();
+
+ // 3. Every protected route: a missing key must NEVER return a 2xx. Past the
+ // limiter budget the denial may be 429 instead of 401 — both are fine,
+ // the invariant is "unauthenticated requests never succeed".
+ for (const [method, path] of PROTECTED_ROUTES) {
+ const res = await fetch(`${url}${path}`, {
+ method,
+ headers: method === "POST" ? { "Content-Type": "application/json" } : undefined,
+ body: method === "POST" ? "{}" : undefined,
+ });
+ await res.text();
+ assert.ok(
+ res.status === 401 || res.status === 429,
+ `${method} ${path} without a key returned ${res.status}; expected 401 or 429 (never a success)`,
+ );
+ assert.ok(res.status < 200 || res.status >= 300, `${method} ${path} must not succeed without a key`);
+ }
+ } finally {
+ await close();
+ }
+});
+
+test("SEC-3: an unrecognized stack timeframe is handled gracefully, not errored", async () => {
+ const { url, close } = await startTestServer();
+ try {
+ const res = await fetch(`${url}/api/stack?timeframe=__junk__`);
+ assert.equal(res.status, 200, "junk timeframe should be coerced and return 200, not error");
+ const body = (await res.json()) as Record;
+ assert.ok(Array.isArray(body.compute), "stack response should still carry layer arrays");
+ } finally {
+ await close();
+ }
+});
diff --git a/server/__tests__/fundamentals.test.ts b/server/__tests__/fundamentals.test.ts
new file mode 100644
index 0000000..bcc236a
--- /dev/null
+++ b/server/__tests__/fundamentals.test.ts
@@ -0,0 +1,94 @@
+// Live fundamentals sweep: conversion guards, bounded-concurrency sweep,
+// per-ticker failure isolation, cache single-flight.
+import { describe, it, beforeEach } from "node:test";
+import assert from "node:assert/strict";
+import {
+ clearFundamentalsCache,
+ fractionToPercent,
+ getCachedFundamentals,
+ refreshFundamentalsIfStale,
+ sweepRevenueGrowth,
+} from "../fundamentals";
+
+describe("fractionToPercent", () => {
+ it("converts Yahoo's fraction to percent", () => {
+ const near = (a: number | null, b: number) => assert.ok(a !== null && Math.abs(a - b) < 1e-9, `${a} !~ ${b}`);
+ near(fractionToPercent(0.122), 12.2);
+ near(fractionToPercent(1.224), 122.4);
+ near(fractionToPercent(-0.05), -5);
+ });
+ it("rejects junk: non-numbers, non-finite, absurd magnitudes", () => {
+ for (const bad of [null, undefined, "12%", NaN, Infinity, 51, -51]) {
+ assert.equal(fractionToPercent(bad), null, String(bad));
+ }
+ });
+});
+
+describe("sweepRevenueGrowth", () => {
+ it("collects rounded values per ticker", async () => {
+ const out = await sweepRevenueGrowth(["A", "B"], async (t) => (t === "A" ? 12.24 : 5.55));
+ assert.equal(out.A.revenueGrowth, 12.2);
+ assert.equal(out.B.revenueGrowth, 5.6);
+ });
+ it("a failing ticker yields null without sinking the sweep", async () => {
+ const out = await sweepRevenueGrowth(["A", "B"], async (t) => {
+ if (t === "A") throw new Error("throttled");
+ return 7;
+ });
+ assert.equal(out.A.revenueGrowth, null);
+ assert.equal(out.B.revenueGrowth, 7);
+ });
+ it("respects the concurrency bound", async () => {
+ let inFlight = 0;
+ let peak = 0;
+ const out = await sweepRevenueGrowth(
+ Array.from({ length: 20 }, (_, i) => `T${i}`),
+ async () => {
+ inFlight++;
+ peak = Math.max(peak, inFlight);
+ await new Promise((r) => setTimeout(r, 5));
+ inFlight--;
+ return 1;
+ },
+ 4,
+ );
+ assert.equal(Object.keys(out).length, 20);
+ assert.ok(peak <= 4, `peak concurrency ${peak}`);
+ });
+ it("empty ticker list yields empty result", async () => {
+ assert.deepEqual(await sweepRevenueGrowth([], async () => 1), {});
+ });
+});
+
+describe("refreshFundamentalsIfStale", () => {
+ beforeEach(() => clearFundamentalsCache());
+ it("populates the cache once and is single-flight while fresh", async () => {
+ let calls = 0;
+ refreshFundamentalsIfStale(["A"], async () => {
+ calls++;
+ return 3;
+ });
+ // second trigger while the first is in flight must not double-fetch
+ refreshFundamentalsIfStale(["A"], async () => {
+ calls++;
+ return 3;
+ });
+ await new Promise((r) => setTimeout(r, 20));
+ assert.equal(calls, 1);
+ assert.equal(getCachedFundamentals().A.revenueGrowth, 3);
+ // fresh cache: no new sweep
+ refreshFundamentalsIfStale(["A"], async () => {
+ calls++;
+ return 9;
+ });
+ await new Promise((r) => setTimeout(r, 20));
+ assert.equal(calls, 1);
+ });
+ it("a failed sweep leaves the cache empty and allows retry", async () => {
+ refreshFundamentalsIfStale(["A"], async () => {
+ throw new Error("down");
+ });
+ await new Promise((r) => setTimeout(r, 20));
+ assert.equal(getCachedFundamentals().A?.revenueGrowth ?? null, null);
+ });
+});
diff --git a/server/__tests__/gpu-live.test.ts b/server/__tests__/gpu-live.test.ts
new file mode 100644
index 0000000..21e9737
--- /dev/null
+++ b/server/__tests__/gpu-live.test.ts
@@ -0,0 +1,155 @@
+// Live GPU price ingestion: parsing, blending, guard rails, and the full
+// sweep against fixture provider payloads. A bad SKU match here would put a
+// wrong price on the front page, so the guard rails get their own cases.
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+import {
+ RUNPOD_MODEL_IDS,
+ blendLivePrice,
+ fetchLivePrices,
+ parseRunpod,
+ parseVast,
+ type FetchLike,
+ type RunpodGpuType,
+} from "../gpu-live";
+
+describe("parseRunpod", () => {
+ const types: RunpodGpuType[] = [
+ { id: "NVIDIA H100 80GB HBM3", securePrice: 3.29, communityPrice: 2.69 },
+ { id: "NVIDIA H100 PCIe", securePrice: 2.89, communityPrice: 1.99 },
+ { id: "NVIDIA H200", securePrice: 4.39, communityPrice: 0.5 },
+ { id: "NVIDIA B200", securePrice: null, communityPrice: 5.98 },
+ ];
+ it("matches only the mapped SKU ids", () => {
+ const obs = parseRunpod(types, RUNPOD_MODEL_IDS.H100);
+ assert.deepEqual(obs.map((o) => o.price), [3.29, 2.69]);
+ });
+ it("drops placeholder community prices (under 40% of secure)", () => {
+ const obs = parseRunpod(types, ["NVIDIA H200"]);
+ assert.deepEqual(obs.map((o) => o.price), [4.39]);
+ });
+ it("community without a secure anchor is dropped entirely", () => {
+ const obs = parseRunpod(types, ["NVIDIA B200"]);
+ assert.deepEqual(obs, []);
+ });
+ it("empty/missing input yields no observations", () => {
+ assert.deepEqual(parseRunpod([], ["x"]), []);
+ assert.deepEqual(parseRunpod(undefined as never, ["x"]), []);
+ });
+});
+
+describe("parseVast", () => {
+ it("takes the median of returned offers", () => {
+ const obs = parseVast([{ dph_total: 1.74 }, { dph_total: 1.93 }, { dph_total: 2.0 }]);
+ assert.deepEqual(obs, [{ provider: "vast", price: 1.93 }]);
+ });
+ it("even count averages the middle pair", () => {
+ const obs = parseVast([{ dph_total: 1.0 }, { dph_total: 2.0 }, { dph_total: 3.0 }, { dph_total: 4.0 }]);
+ assert.equal(obs[0].price, 2.5);
+ });
+ it("ignores null/zero/negative totals; empty yields nothing", () => {
+ assert.deepEqual(parseVast([{ dph_total: null }, { dph_total: 0 }, { dph_total: -1 }]), []);
+ assert.deepEqual(parseVast([]), []);
+ });
+});
+
+describe("blendLivePrice", () => {
+ it("median blend with low/high and deduped sources", () => {
+ const { blended, dropped } = blendLivePrice(
+ [
+ { provider: "runpod-secure", price: 3.29 },
+ { provider: "runpod-community", price: 2.69 },
+ { provider: "vast", price: 1.93 },
+ ],
+ 2.79,
+ );
+ assert.equal(dropped, 0);
+ assert.ok(blended);
+ assert.equal(blended.price, 2.69);
+ assert.equal(blended.low, 1.93);
+ assert.equal(blended.high, 3.29);
+ assert.equal(blended.n, 3);
+ assert.deepEqual(blended.sources.sort(), ["runpod-community", "runpod-secure", "vast"]);
+ });
+ it("guard rail: observations 4x off the curated price are dropped as bad matches", () => {
+ const { blended, dropped } = blendLivePrice(
+ [
+ { provider: "vast", price: 0.4 }, // 10x cheaper than curated: wrong SKU
+ { provider: "runpod-secure", price: 30 }, // 7x pricier: glitch
+ { provider: "runpod-community", price: 4.1 },
+ ],
+ 4.0,
+ );
+ assert.equal(dropped, 2);
+ assert.equal(blended?.price, 4.1);
+ });
+ it("no curated reference means no ratio guard (new models still record)", () => {
+ const { blended, dropped } = blendLivePrice([{ provider: "vast", price: 42 }], null);
+ assert.equal(dropped, 0);
+ assert.equal(blended?.price, 42);
+ });
+ it("all observations dropped or empty yields null (record nothing, fabricate nothing)", () => {
+ assert.equal(blendLivePrice([], 3).blended, null);
+ assert.equal(blendLivePrice([{ provider: "vast", price: NaN }], 3).blended, null);
+ });
+});
+
+describe("fetchLivePrices (full sweep against fixtures)", () => {
+ const runpodBody = {
+ data: {
+ gpuTypes: [
+ { id: "NVIDIA H100 80GB HBM3", securePrice: 3.29, communityPrice: 2.69 },
+ { id: "NVIDIA B200", securePrice: 5.89, communityPrice: 5.98 },
+ ],
+ },
+ };
+ const vastByName: Record = {
+ "H100 SXM": { offers: [{ dph_total: 1.74 }, { dph_total: 1.93 }, { dph_total: 2.0 }] },
+ };
+ const fakeFetch: FetchLike = async (url) => {
+ if (url.includes("runpod")) return { ok: true, json: async () => runpodBody };
+ const q = JSON.parse(decodeURIComponent(url.split("?q=")[1]));
+ return { ok: true, json: async () => vastByName[q.gpu_name.eq] ?? { offers: [] } };
+ };
+
+ it("blends per model across providers; models with no source record nothing", async () => {
+ const live = await fetchLivePrices(
+ [
+ { model: "H100", currentUsdPerHr: 2.79 },
+ { model: "B200", currentUsdPerHr: 6.11 },
+ { model: "MI355X", currentUsdPerHr: 2.85 }, // no provider covers it
+ ],
+ fakeFetch,
+ );
+ assert.deepEqual(Object.keys(live).sort(), ["B200", "H100"]);
+ assert.equal(live.H100.price, 2.69); // median of 3.29, 2.69, 1.93
+ assert.equal(live.H100.n, 3);
+ assert.equal(live.B200.n, 2);
+ assert.equal("MI355X" in live, false);
+ });
+
+ it("provider failure degrades to the surviving sources, never throws", async () => {
+ const failingFetch: FetchLike = async (url) => {
+ if (url.includes("runpod")) throw new Error("network down");
+ const q = JSON.parse(decodeURIComponent(url.split("?q=")[1]));
+ return { ok: true, json: async () => vastByName[q.gpu_name.eq] ?? { offers: [] } };
+ };
+ const live = await fetchLivePrices([{ model: "H100", currentUsdPerHr: 2.79 }], failingFetch);
+ assert.equal(live.H100.price, 1.93); // vast only
+ assert.deepEqual(live.H100.sources, ["vast"]);
+ });
+
+ it("total failure yields an empty result (nothing recorded, nothing invented)", async () => {
+ const deadFetch: FetchLike = async () => {
+ throw new Error("offline");
+ };
+ const live = await fetchLivePrices([{ model: "H100", currentUsdPerHr: 2.79 }], deadFetch);
+ assert.deepEqual(live, {});
+ });
+
+ it("non-ok responses are treated as empty, not parsed", async () => {
+ const badFetch: FetchLike = async () => ({ ok: false, json: async () => ({ oops: true }) });
+ const live = await fetchLivePrices([{ model: "H100", currentUsdPerHr: 2.79 }], badFetch);
+ assert.deepEqual(live, {});
+ });
+});
diff --git a/server/__tests__/uranium-correlation.test.ts b/server/__tests__/uranium-correlation.test.ts
new file mode 100644
index 0000000..d4f9d60
--- /dev/null
+++ b/server/__tests__/uranium-correlation.test.ts
@@ -0,0 +1,108 @@
+// The real uranium correlation module - the replacement for the app's last
+// fabricated dataset. Pearson math, weekly alignment, thresholds, cache and
+// failure behavior all covered.
+import { describe, it, beforeEach } from "node:test";
+import assert from "node:assert/strict";
+import {
+ alignWeekly,
+ buildCorrelationPayload,
+ clearCorrelationCache,
+ getUraniumCorrelation,
+ pearson,
+ type WeeklyPoint,
+} from "../uranium-correlation";
+
+const WEEK = 7 * 86_400_000;
+const series = (closes: Array, startWeek = 3000): WeeklyPoint[] =>
+ closes.map((c, i) => ({ t: (startWeek + i) * WEEK + 3 * 86_400_000, close: c as number }));
+
+describe("pearson", () => {
+ it("perfect positive and negative correlation", () => {
+ assert.equal(pearson([1, 2, 3, 4], [2, 4, 6, 8]), 1);
+ assert.equal(pearson([1, 2, 3, 4], [8, 6, 4, 2]), -1);
+ });
+ it("known value on a hand-computed set", () => {
+ const r = pearson([1, 2, 3, 4, 5], [2, 1, 4, 3, 5]);
+ assert.ok(r !== null && Math.abs(r - 0.8) < 1e-9);
+ });
+ it("null on n<3 or zero variance", () => {
+ assert.equal(pearson([1, 2], [1, 2]), null);
+ assert.equal(pearson([5, 5, 5], [1, 2, 3]), null);
+ assert.equal(pearson([1, 2, 3], [7, 7, 7]), null);
+ });
+});
+
+describe("alignWeekly", () => {
+ it("pairs closes that fall in the same calendar week", () => {
+ const a = series([10, 11, 12]);
+ const b = series([100, 110, 120]);
+ const out = alignWeekly(a, b);
+ assert.deepEqual(out.map((p) => [p.a, p.b]), [[10, 100], [11, 110], [12, 120]]);
+ });
+ it("weeks missing from either side are dropped, never interpolated", () => {
+ const a = series([10, 11, 12]);
+ const b = [series([100])[0], series([120], 3002)[0]]; // week 3001 missing
+ const out = alignWeekly(a, b);
+ assert.deepEqual(out.map((p) => [p.a, p.b]), [[10, 100], [12, 120]]);
+ });
+ it("non-finite closes are excluded", () => {
+ const a = series([10, NaN, 12]);
+ const b = series([100, 110, null]);
+ const out = alignWeekly(a, b);
+ assert.deepEqual(out.map((p) => [p.a, p.b]), [[10, 100]]);
+ });
+ it("empty inputs yield empty output", () => {
+ assert.deepEqual(alignWeekly([], series([1, 2, 3])), []);
+ assert.deepEqual(alignWeekly(series([1]), []), []);
+ });
+});
+
+describe("buildCorrelationPayload", () => {
+ const proxy = series(Array.from({ length: 52 }, (_, i) => 15 + i * 0.1));
+ const ccj = series(Array.from({ length: 52 }, (_, i) => 70 + i * 0.5));
+ const ceg = series(Array.from({ length: 52 }, (_, i) => 300 - i));
+ it("pairs, rounds, and computes real r for both relationships", () => {
+ const p = buildCorrelationPayload(proxy, ccj, ceg, "SRUUF", "2026-07-04");
+ assert.ok(p);
+ assert.equal(p.weeks, 52);
+ assert.equal(p.ccjPairs.length, 52);
+ assert.ok(p.ccjR !== null && p.ccjR > 0.999); // both strictly increasing
+ assert.ok(p.cegR !== null && p.cegR < -0.999); // proxy up, ceg down
+ assert.equal(p.proxyTicker, "SRUUF");
+ });
+ it("null when overlap is too thin to present as a year (<20 weeks)", () => {
+ const p = buildCorrelationPayload(series([1, 2, 3]), series([4, 5, 6]), [], "SRUUF", "x");
+ assert.equal(p, null);
+ });
+ it("CEG side degrades to null independently when its overlap is thin", () => {
+ const p = buildCorrelationPayload(proxy, ccj, series([1, 2, 3]), "SRUUF", "x");
+ assert.ok(p);
+ assert.equal(p.cegR, null);
+ assert.equal(p.cegPairs.length, 3);
+ });
+});
+
+describe("getUraniumCorrelation (cache + failure)", () => {
+ beforeEach(() => clearCorrelationCache());
+ const good = async (t: string) =>
+ series(Array.from({ length: 52 }, (_, i) => (t === "SRUUF" ? 15 + i * 0.1 : t === "CCJ" ? 70 + i : 300 + i)));
+
+ it("computes once and serves from cache after", async () => {
+ let calls = 0;
+ const counting = async (t: string) => {
+ calls++;
+ return good(t);
+ };
+ const p1 = await getUraniumCorrelation(counting);
+ const p2 = await getUraniumCorrelation(counting);
+ assert.ok(p1 && p2);
+ assert.equal(calls, 3); // one sweep of three tickers, second call cached
+ });
+ it("fetch failure yields null - nothing invented", async () => {
+ const dead = async () => {
+ throw new Error("yahoo down");
+ };
+ const p = await getUraniumCorrelation(dead);
+ assert.equal(p, null);
+ });
+});
diff --git a/server/__tests__/weekly-digest.test.ts b/server/__tests__/weekly-digest.test.ts
new file mode 100644
index 0000000..4449e8f
--- /dev/null
+++ b/server/__tests__/weekly-digest.test.ts
@@ -0,0 +1,86 @@
+// Weekly digest render: structure, personalization hook, escaping, honest
+// omission of missing gauges, date label math.
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+import { renderWeeklyEmail, weeklyDateLabel, type WeeklyDigestInput } from "../weekly-digest";
+
+const INPUT: WeeklyDigestInput = {
+ brief: {
+ title: "The Buildout Brief",
+ asOf: "2026-07-04",
+ summary: "Tracked clusters now total 118 GW planned across 77 operators.",
+ sections: [
+ { heading: "Compute", points: ["235 clusters tracked.", "Largest: Abilene at 1.2 GW."] },
+ { heading: "Grid", points: ["Queue at 2,290 GW; median wait 55 months."] },
+ ],
+ takeaway: "Power, not chips, is the binding constraint this quarter.",
+ },
+ movers: [
+ { ticker: "NVDA", name: "NVIDIA Corporation", changePercent: -1.39 },
+ { ticker: "AAPL", name: "Apple Inc.