diff --git a/.gitignore b/.gitignore index 70e48a1..c3b779b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ attached_assets/Pasted-* /tmp/ coverage/ .nyc_output/ + +.card-preview/ diff --git a/CLAUDE.md b/CLAUDE.md index e183779..a5efd9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ The G-chord list is maintained twice in App.tsx (display ~67-83, handler ~250-25 d3 ^7.9.0 (Supply Chain force graph only). - Server libs: yahoo-finance2 ^3.13.1, rss-parser, satori + @resvg/resvg-js (OG cards), zod, helmet, express-rate-limit. -- Tests: Node's built-in runner. `npm test` = server/__tests__ + client/src/lib/__tests__ (217 tests). No component/DOM tests. +- Tests: Node's built-in runner. `npm test` = server/__tests__ + client/src/lib/__tests__ (300 tests). No component/DOM tests. - Scripts: `dev` (tsx), `build` (script/build.ts: vite client + esbuild server to dist/index.cjs), `start`, `check` (tsc), `test`, `backtest:indices`. - Persistence: JSON in server/data/ plus content/blog/articles.json. There is NO database, @@ -73,7 +73,7 @@ Derived from the code; starred rules confirmed by Jack 2026-07-02. marketing only. The landing has its own scoped tokens (.gt-marketing, client/src/styles/anchor.css). - Server layout: every route lives in server/routes.ts; math and fetching live in small pure modules routes call (indices.ts, clusters.ts, deals.ts, gpu-index.ts, gpu-economics.ts, - gpu-history.ts, brief.ts, physical.ts, social-format.ts). New module = pure module + thin + gpu-history.ts, brief.ts, physical.ts, social-format.ts, og-card.ts). New module = pure module + thin route + test. server/physical.ts is the house template: constant URL, in-memory TTL cache, typed honest degradation ({configured:false} when keyless, 502 on failure), never fabricate. - Frontier model data is public at `/api/frontier-models`. Benchmark records require a cited @@ -122,6 +122,38 @@ server/data custody (the fragility map): admin routes write into it). gpu-rental-prices.json is rewritten weekly by n8n; schema changes break the unattended refresh. +## 4b. Social / OG cards + +server/og-card.ts owns the layout, fonts and map for every card (/api/og and the +daily X post). routes.ts only gathers data; ogCardForTemplate returns an OgCard. + +- Every card MUST carry `asOf` and `source`. They are required fields, not + optional decoration: the product claim is "tracked with sourced numbers", so a + card showing a number without a date and a provenance line contradicts it. + Missing asOf prints "AS OF —"; never substitute today's date. +- Fonts: Inter-Regular.ttf (400), Inter-Bold.woff (700), JetBrainsMono-Bold.woff + (700), all in server/fonts/, all registered in og-card.ts. satori CANNOT + synthesize weights. Asking for a weight or family that is not registered + renders regular silently, which is how the pre-2026-08 card shipped flat for + months. server/__tests__/og-card.test.ts fails if a face goes missing. +- satori silently TRUNCATES large inline SVG markup. The US outline must stay a + single concatenated path; per-state elements exceeded the limit and + rendered a country missing most of its states with no error. Guarded by + assertMapPathIntact(). +- server/us-map.ts is GENERATED by scripts/generate-map-path.mjs. It carries the + outline path plus closed-form Albers constants, so the server projects + coordinates with plain arithmetic and never imports d3-geo, which is ESM-only + and would throw ERR_REQUIRE_ESM from the CJS bundle on older Node. d3-geo is a + devDependency used only by that script and the parity test. +- `npx tsx scripts/preview-cards.ts` renders every template to .card-preview/ + for eyeballing. It posts nothing. +- client/src/data/us-states.geo.json is the source geometry. The copy that + shipped on the elevate branch had Virginia's outer ring wound backwards, which + d3 reads as "the whole sphere minus Virginia". geoAlbersUsa's clipping hid it; + any non-composite projection does not. The committed copy is rewound, and the + generator rewinds + warns if a bad ring ever reappears. Client-side map work + should use this file, not the elevate copy. + ## 5. Do not touch - server/indices.ts constants (baselines, gains, clamps, NPI_BASE anchors, weights): published diff --git a/client/src/data/us-states.geo.json b/client/src/data/us-states.geo.json index 1c1ebe5..40f3325 100644 --- a/client/src/data/us-states.geo.json +++ b/client/src/data/us-states.geo.json @@ -45,7 +45,7 @@ {"type":"Feature","id":"48","properties":{"name":"Texas","density":98.07},"geometry":{"type":"Polygon","coordinates":[[[-101.812942,36.501861],[-100.000075,36.501861],[-100.000075,34.563024],[-99.923398,34.573978],[-99.698843,34.382285],[-99.57835,34.415147],[-99.260688,34.404193],[-99.189488,34.2125],[-98.986841,34.223454],[-98.767763,34.135823],[-98.570593,34.146777],[-98.488439,34.064623],[-98.36247,34.157731],[-98.170777,34.113915],[-98.088623,34.004376],[-97.946222,33.987946],[-97.869545,33.851022],[-97.694283,33.982469],[-97.458774,33.905791],[-97.371143,33.823637],[-97.256128,33.861976],[-97.173974,33.736006],[-96.922034,33.960561],[-96.850834,33.845545],[-96.631756,33.845545],[-96.423633,33.774345],[-96.346956,33.686714],[-96.149786,33.840068],[-95.936185,33.889361],[-95.8376,33.834591],[-95.602092,33.933176],[-95.547322,33.878407],[-95.289906,33.87293],[-95.224183,33.960561],[-94.966767,33.861976],[-94.868182,33.74696],[-94.484796,33.637421],[-94.380734,33.544313],[-94.183564,33.593606],[-94.041164,33.54979],[-94.041164,33.018527],[-94.041164,31.994339],[-93.822086,31.775262],[-93.816609,31.556184],[-93.542762,31.15089],[-93.526331,30.93729],[-93.630393,30.679874],[-93.728978,30.575812],[-93.696116,30.438888],[-93.767317,30.334826],[-93.690639,30.143133],[-93.926148,29.787132],[-93.838517,29.688547],[-94.002825,29.68307],[-94.523134,29.546147],[-94.70935,29.622824],[-94.742212,29.787132],[-94.873659,29.672117],[-94.966767,29.699501],[-95.016059,29.557101],[-94.911997,29.496854],[-94.895566,29.310638],[-95.081782,29.113469],[-95.383014,28.867006],[-95.985477,28.604113],[-96.045724,28.647929],[-96.226463,28.582205],[-96.23194,28.642452],[-96.478402,28.598636],[-96.593418,28.724606],[-96.664618,28.697221],[-96.401725,28.439805],[-96.593418,28.357651],[-96.774157,28.406943],[-96.801542,28.226204],[-97.026096,28.039988],[-97.256128,27.694941],[-97.404005,27.333463],[-97.513544,27.360848],[-97.540929,27.229401],[-97.425913,27.262263],[-97.480682,26.99937],[-97.557359,26.988416],[-97.562836,26.840538],[-97.469728,26.758384],[-97.442344,26.457153],[-97.332805,26.353091],[-97.30542,26.161398],[-97.217789,25.991613],[-97.524498,25.887551],[-97.650467,26.018997],[-97.885976,26.06829],[-98.198161,26.057336],[-98.466531,26.221644],[-98.669178,26.238075],[-98.822533,26.369522],[-99.030656,26.413337],[-99.173057,26.539307],[-99.266165,26.840538],[-99.446904,27.021277],[-99.424996,27.174632],[-99.50715,27.33894],[-99.479765,27.48134],[-99.605735,27.640172],[-99.709797,27.656603],[-99.879582,27.799003],[-99.934351,27.979742],[-100.082229,28.14405],[-100.29583,28.280974],[-100.399891,28.582205],[-100.498476,28.66436],[-100.629923,28.905345],[-100.673738,29.102515],[-100.799708,29.244915],[-101.013309,29.370885],[-101.062601,29.458516],[-101.259771,29.535193],[-101.413125,29.754271],[-101.851281,29.803563],[-102.114174,29.792609],[-102.338728,29.869286],[-102.388021,29.765225],[-102.629006,29.732363],[-102.809745,29.524239],[-102.919284,29.190146],[-102.97953,29.184669],[-103.116454,28.987499],[-103.280762,28.982022],[-103.527224,29.135376],[-104.146119,29.381839],[-104.266611,29.513285],[-104.507597,29.639255],[-104.677382,29.924056],[-104.688336,30.181472],[-104.858121,30.389596],[-104.896459,30.570335],[-105.005998,30.685351],[-105.394861,30.855136],[-105.602985,31.085167],[-105.77277,31.167321],[-105.953509,31.364491],[-106.205448,31.468553],[-106.38071,31.731446],[-106.528588,31.786216],[-106.643603,31.901231],[-106.616219,31.999816],[-103.067161,31.999816],[-103.067161,33.002096],[-103.045254,34.01533],[-103.039777,36.501861],[-103.001438,36.501861],[-101.812942,36.501861]]]}}, {"type":"Feature","id":"49","properties":{"name":"Utah","density":34.30},"geometry":{"type":"Polygon","coordinates":[[[-112.164359,41.995232],[-111.047063,42.000709],[-111.047063,40.998429],[-109.04798,40.998429],[-109.053457,39.125316],[-109.058934,38.27639],[-109.042503,38.166851],[-109.042503,37.000263],[-110.499369,37.00574],[-114.048427,37.000263],[-114.04295,41.995232],[-112.164359,41.995232]]]}}, {"type":"Feature","id":"50","properties":{"name":"Vermont","density":67.73},"geometry":{"type":"Polygon","coordinates":[[[-71.503554,45.013027],[-71.4926,44.914442],[-71.629524,44.750133],[-71.536416,44.585825],[-71.700724,44.41604],[-72.034817,44.322932],[-72.02934,44.07647],[-72.116971,43.994316],[-72.204602,43.769761],[-72.379864,43.572591],[-72.456542,43.150867],[-72.445588,43.008466],[-72.533219,42.953697],[-72.544173,42.80582],[-72.456542,42.729142],[-73.267129,42.745573],[-73.278083,42.833204],[-73.245221,43.523299],[-73.404052,43.687607],[-73.349283,43.769761],[-73.436914,44.043608],[-73.321898,44.246255],[-73.294514,44.437948],[-73.387622,44.618687],[-73.332852,44.804903],[-73.343806,45.013027],[-72.308664,45.002073],[-71.503554,45.013027]]]}}, -{"type":"Feature","id":"51","properties":{"name":"Virginia","density":204.5},"geometry":{"type":"MultiPolygon","coordinates":[[[[-75.397659,38.013497],[-75.244304,38.029928],[-75.375751,37.860142],[-75.512674,37.799896],[-75.594828,37.569865],[-75.802952,37.197433],[-75.972737,37.120755],[-76.027507,37.257679],[-75.939876,37.564388],[-75.671506,37.95325],[-75.397659,38.013497]]],[[[-76.016553,37.95325],[-75.994645,37.95325],[-76.043938,37.95325],[-76.016553,37.95325]]],[[[-78.349729,39.464886],[-77.82942,39.130793],[-77.719881,39.322485],[-77.566527,39.306055],[-77.456988,39.223901],[-77.456988,39.076023],[-77.248864,39.026731],[-77.117418,38.933623],[-77.040741,38.791222],[-77.128372,38.632391],[-77.248864,38.588575],[-77.325542,38.446175],[-77.281726,38.342113],[-77.013356,38.374975],[-76.964064,38.216144],[-76.613539,38.15042],[-76.514954,38.024451],[-76.235631,37.887527],[-76.3616,37.608203],[-76.246584,37.389126],[-76.383508,37.285064],[-76.399939,37.159094],[-76.273969,37.082417],[-76.410893,36.961924],[-76.619016,37.120755],[-76.668309,37.065986],[-76.48757,36.95097],[-75.994645,36.923586],[-75.868676,36.551154],[-79.510841,36.5402],[-80.294043,36.545677],[-80.978661,36.562108],[-81.679709,36.589492],[-83.673316,36.600446],[-83.136575,36.742847],[-83.070852,36.852385],[-82.879159,36.890724],[-82.868205,36.978355],[-82.720328,37.044078],[-82.720328,37.120755],[-82.353373,37.268633],[-81.969987,37.537003],[-81.986418,37.454849],[-81.849494,37.285064],[-81.679709,37.20291],[-81.55374,37.208387],[-81.362047,37.339833],[-81.225123,37.235771],[-80.967707,37.290541],[-80.513121,37.482234],[-80.474782,37.421987],[-80.29952,37.509618],[-80.294043,37.690357],[-80.184505,37.849189],[-79.998289,37.997066],[-79.921611,38.177805],[-79.724442,38.364021],[-79.647764,38.594052],[-79.477979,38.457129],[-79.313671,38.413313],[-79.209609,38.495467],[-78.996008,38.851469],[-78.870039,38.763838],[-78.404499,39.169131],[-78.349729,39.464886]]]]}}, +{"type":"Feature","id":"51","properties":{"name":"Virginia","density":204.5},"geometry":{"type":"MultiPolygon","coordinates":[[[[-75.397659,38.013497],[-75.244304,38.029928],[-75.375751,37.860142],[-75.512674,37.799896],[-75.594828,37.569865],[-75.802952,37.197433],[-75.972737,37.120755],[-76.027507,37.257679],[-75.939876,37.564388],[-75.671506,37.95325],[-75.397659,38.013497]]],[[[-76.016553,37.95325],[-76.043938,37.95325],[-75.994645,37.95325],[-76.016553,37.95325]]],[[[-78.349729,39.464886],[-77.82942,39.130793],[-77.719881,39.322485],[-77.566527,39.306055],[-77.456988,39.223901],[-77.456988,39.076023],[-77.248864,39.026731],[-77.117418,38.933623],[-77.040741,38.791222],[-77.128372,38.632391],[-77.248864,38.588575],[-77.325542,38.446175],[-77.281726,38.342113],[-77.013356,38.374975],[-76.964064,38.216144],[-76.613539,38.15042],[-76.514954,38.024451],[-76.235631,37.887527],[-76.3616,37.608203],[-76.246584,37.389126],[-76.383508,37.285064],[-76.399939,37.159094],[-76.273969,37.082417],[-76.410893,36.961924],[-76.619016,37.120755],[-76.668309,37.065986],[-76.48757,36.95097],[-75.994645,36.923586],[-75.868676,36.551154],[-79.510841,36.5402],[-80.294043,36.545677],[-80.978661,36.562108],[-81.679709,36.589492],[-83.673316,36.600446],[-83.136575,36.742847],[-83.070852,36.852385],[-82.879159,36.890724],[-82.868205,36.978355],[-82.720328,37.044078],[-82.720328,37.120755],[-82.353373,37.268633],[-81.969987,37.537003],[-81.986418,37.454849],[-81.849494,37.285064],[-81.679709,37.20291],[-81.55374,37.208387],[-81.362047,37.339833],[-81.225123,37.235771],[-80.967707,37.290541],[-80.513121,37.482234],[-80.474782,37.421987],[-80.29952,37.509618],[-80.294043,37.690357],[-80.184505,37.849189],[-79.998289,37.997066],[-79.921611,38.177805],[-79.724442,38.364021],[-79.647764,38.594052],[-79.477979,38.457129],[-79.313671,38.413313],[-79.209609,38.495467],[-78.996008,38.851469],[-78.870039,38.763838],[-78.404499,39.169131],[-78.349729,39.464886]]]]}}, {"type":"Feature","id":"53","properties":{"name":"Washington","density":102.6},"geometry":{"type":"MultiPolygon","coordinates":[[[[-117.033359,49.000239],[-117.044313,47.762451],[-117.038836,46.426077],[-117.055267,46.343923],[-116.92382,46.168661],[-116.918344,45.993399],[-118.988627,45.998876],[-119.125551,45.933153],[-119.525367,45.911245],[-119.963522,45.823614],[-120.209985,45.725029],[-120.505739,45.697644],[-120.637186,45.746937],[-121.18488,45.604536],[-121.217742,45.670259],[-121.535404,45.725029],[-121.809251,45.708598],[-122.247407,45.549767],[-122.762239,45.659305],[-122.811531,45.960537],[-122.904639,46.08103],[-123.11824,46.185092],[-123.211348,46.174138],[-123.370179,46.146753],[-123.545441,46.261769],[-123.72618,46.300108],[-123.874058,46.239861],[-124.065751,46.327492],[-124.027412,46.464416],[-123.895966,46.535616],[-124.098612,46.74374],[-124.235536,47.285957],[-124.31769,47.357157],[-124.427229,47.740543],[-124.624399,47.88842],[-124.706553,48.184175],[-124.597014,48.381345],[-124.394367,48.288237],[-123.983597,48.162267],[-123.704273,48.167744],[-123.424949,48.118452],[-123.162056,48.167744],[-123.036086,48.080113],[-122.800578,48.08559],[-122.636269,47.866512],[-122.515777,47.882943],[-122.493869,47.587189],[-122.422669,47.318818],[-122.324084,47.346203],[-122.422669,47.576235],[-122.395284,47.800789],[-122.230976,48.030821],[-122.362422,48.123929],[-122.373376,48.288237],[-122.471961,48.468976],[-122.422669,48.600422],[-122.488392,48.753777],[-122.647223,48.775685],[-122.795101,48.8907],[-122.756762,49.000239],[-117.033359,49.000239]]],[[[-122.718423,48.310145],[-122.586977,48.35396],[-122.608885,48.151313],[-122.767716,48.227991],[-122.718423,48.310145]]],[[[-123.025132,48.583992],[-122.915593,48.715438],[-122.767716,48.556607],[-122.811531,48.419683],[-123.041563,48.458022],[-123.025132,48.583992]]]]}}, {"type":"Feature","id":"54","properties":{"name":"West Virginia","density":77.06},"geometry":{"type":"Polygon","coordinates":[[[-80.518598,40.636951],[-80.518598,39.722302],[-79.477979,39.722302],[-79.488933,39.20747],[-79.291763,39.300578],[-79.094593,39.470363],[-78.963147,39.437501],[-78.765977,39.585379],[-78.470222,39.514178],[-78.431884,39.623717],[-78.267575,39.61824],[-78.174467,39.694917],[-78.004682,39.601809],[-77.834897,39.601809],[-77.719881,39.322485],[-77.82942,39.130793],[-78.349729,39.464886],[-78.404499,39.169131],[-78.870039,38.763838],[-78.996008,38.851469],[-79.209609,38.495467],[-79.313671,38.413313],[-79.477979,38.457129],[-79.647764,38.594052],[-79.724442,38.364021],[-79.921611,38.177805],[-79.998289,37.997066],[-80.184505,37.849189],[-80.294043,37.690357],[-80.29952,37.509618],[-80.474782,37.421987],[-80.513121,37.482234],[-80.967707,37.290541],[-81.225123,37.235771],[-81.362047,37.339833],[-81.55374,37.208387],[-81.679709,37.20291],[-81.849494,37.285064],[-81.986418,37.454849],[-81.969987,37.537003],[-82.101434,37.553434],[-82.293127,37.668449],[-82.342419,37.783465],[-82.50125,37.931343],[-82.621743,38.123036],[-82.594358,38.424267],[-82.331465,38.446175],[-82.293127,38.577622],[-82.172634,38.632391],[-82.221926,38.785745],[-82.03571,39.026731],[-81.887833,38.873376],[-81.783771,38.966484],[-81.811156,39.0815],[-81.685186,39.273193],[-81.57017,39.267716],[-81.455155,39.410117],[-81.345616,39.344393],[-81.219646,39.388209],[-80.830783,39.711348],[-80.737675,40.078303],[-80.600752,40.319289],[-80.595275,40.472643],[-80.666475,40.582182],[-80.518598,40.636951]]]}}, {"type":"Feature","id":"55","properties":{"name":"Wisconsin","density":105.2},"geometry":{"type":"Polygon","coordinates":[[[-90.415429,46.568478],[-90.229213,46.508231],[-90.119674,46.338446],[-89.09001,46.135799],[-88.662808,45.987922],[-88.531362,46.020784],[-88.10416,45.922199],[-87.989145,45.796229],[-87.781021,45.675736],[-87.791975,45.500474],[-87.885083,45.363551],[-87.649574,45.341643],[-87.742682,45.199243],[-87.589328,45.095181],[-87.627666,44.974688],[-87.819359,44.95278],[-87.983668,44.722749],[-88.043914,44.563917],[-87.928898,44.536533],[-87.775544,44.640595],[-87.611236,44.837764],[-87.403112,44.914442],[-87.238804,45.166381],[-87.03068,45.22115],[-87.047111,45.089704],[-87.189511,44.969211],[-87.468835,44.552964],[-87.545512,44.322932],[-87.540035,44.158624],[-87.644097,44.103854],[-87.737205,43.8793],[-87.704344,43.687607],[-87.791975,43.561637],[-87.912467,43.249452],[-87.885083,43.002989],[-87.76459,42.783912],[-87.802929,42.493634],[-88.788778,42.493634],[-90.639984,42.510065],[-90.711184,42.636034],[-91.067185,42.75105],[-91.143862,42.909881],[-91.176724,43.134436],[-91.056231,43.254929],[-91.204109,43.353514],[-91.215062,43.501391],[-91.269832,43.616407],[-91.242447,43.775238],[-91.43414,43.994316],[-91.592971,44.032654],[-91.877772,44.202439],[-91.927065,44.333886],[-92.233773,44.443425],[-92.337835,44.552964],[-92.545959,44.569394],[-92.808852,44.750133],[-92.737652,45.117088],[-92.75956,45.286874],[-92.644544,45.440228],[-92.770513,45.566198],[-92.885529,45.577151],[-92.869098,45.719552],[-92.639067,45.933153],[-92.354266,46.015307],[-92.29402,46.075553],[-92.29402,46.667063],[-92.091373,46.749217],[-92.014696,46.705401],[-91.790141,46.694447],[-91.09457,46.864232],[-90.837154,46.95734],[-90.749522,46.88614],[-90.886446,46.754694],[-90.55783,46.584908],[-90.415429,46.568478]]]}}, diff --git a/package-lock.json b/package-lock.json index c8491ae..bd0b1ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.20", + "d3-geo": "^3.1.0", "esbuild": "^0.25.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.17", diff --git a/package.json b/package.json index 2a07c1b..a701f92 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.20", + "d3-geo": "^3.1.0", "esbuild": "^0.25.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.17", diff --git a/scripts/generate-map-path.mjs b/scripts/generate-map-path.mjs new file mode 100644 index 0000000..d207b0c --- /dev/null +++ b/scripts/generate-map-path.mjs @@ -0,0 +1,146 @@ +// Bakes the continental-US outline used by the daily social card. +// +// Run: node scripts/generate-map-path.mjs +// Writes server/us-map.ts (committed). Re-run only if the card's map box +// changes size or client/src/data/us-states.geo.json is replaced. +// +// Why bake it: the server renders cards on demand and should not parse an +// 89 KB geojson or run geoPath per request. Baking the path plus the exact +// scale/translate lets the server rebuild the identical projection with +// geoAlbers().scale(S).translate(T), so cluster dots land on the outline. + +import { readFileSync, writeFileSync } from "fs"; +import { geoAlbers, geoPath, geoArea, geoBounds } from "d3-geo"; + +const MAP_W = 478; +const MAP_H = 290; +const SKIP = ["Alaska", "Hawaii", "Puerto Rico"]; + +const geo = JSON.parse(readFileSync("client/src/data/us-states.geo.json", "utf-8")); + +// Guard: a ring wound the wrong way reads as "the whole sphere minus this +// state" in d3's spherical math, which silently destroys the projection fit. +// Virginia shipped inverted; geoAlbersUsa's clipping hid it. Rewind and warn. +let rewound = 0; +for (const f of geo.features) { + const g = f.geometry; + if (!g) continue; + const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : []; + for (const poly of polys) { + if (poly[0] && geoArea({ type: "Polygon", coordinates: [poly[0]] }) > 2 * Math.PI) { + poly[0].reverse(); + rewound++; + console.warn(`rewound inverted outer ring: ${f.properties?.name}`); + } + } +} + +const continental = { + type: "FeatureCollection", + features: geo.features.filter((f) => !SKIP.includes(f.properties?.name)), +}; + +const [[w, s], [e, n]] = geoBounds(continental); +if (w < -126 || e > -66 || s < 23 || n > 50) { + throw new Error(`continental bounds look wrong: ${JSON.stringify(geoBounds(continental))}`); +} + +const projection = geoAlbers().fitSize([MAP_W, MAP_H], continental); + +// ── Bake the projection so the server needs no d3 at runtime ─────────────── +// +// The server bundle is CJS with every dependency marked external, and d3-geo +// is ESM-only. require()-ing it works on Node 22+ but throws ERR_REQUIRE_ESM +// on older runtimes, and the card render sits inside a try/catch, so a failure +// there would silently post image-less tweets forever. Instead we solve the +// closed-form Albers constants here and ship plain arithmetic. +// +// geoAlbers is conicEqualArea with parallels 29.5/45.5 and rotate([96, 0]): +// lambda' = lambda + 96deg +// rho = sqrt(c - 2n sin(phi)) / n +// theta = n * lambda' +// raw = [rho sin(theta), rho0 - rho cos(theta)] +// screen = [k * rawX + dx, dy - k * rawY] +// n, c and rho0 are analytic; k, dx and dy are solved against d3's own output +// so `center()`'s offset is captured exactly rather than re-derived. +const PARALLEL_0 = (29.5 * Math.PI) / 180; +const PARALLEL_1 = (45.5 * Math.PI) / 180; +const LAMBDA0_DEG = 96; + +const sy0 = Math.sin(PARALLEL_0); +const N = (sy0 + Math.sin(PARALLEL_1)) / 2; +const C = 1 + sy0 * (2 * N - sy0); +const RHO0 = Math.sqrt(C) / N; + +function rawAlbers(lat, lng) { + const lambda = ((lng + LAMBDA0_DEG) * Math.PI) / 180; + const phi = (lat * Math.PI) / 180; + const rho = Math.sqrt(C - 2 * N * Math.sin(phi)) / N; + const theta = lambda * N; + return [rho * Math.sin(theta), RHO0 - rho * Math.cos(theta)]; +} + +// Solve k, dx, dy from two reference points, then verify against many more. +const refA = [40.7128, -74.006]; +const refB = [37.7749, -122.4194]; +const [ax, ay] = rawAlbers(...refA); +const [bx, by] = rawAlbers(...refB); +const [AX, AY] = projection([refA[1], refA[0]]); +const [BX, BY] = projection([refB[1], refB[0]]); +const k = (AX - BX) / (ax - bx); +const dx = AX - k * ax; +const dy = AY + k * ay; + +let worst = 0; +for (let lat = 25; lat <= 49; lat += 1.5) { + for (let lng = -124; lng <= -67; lng += 2.5) { + const [rx, ry] = rawAlbers(lat, lng); + const mine = [k * rx + dx, dy - k * ry]; + const theirs = projection([lng, lat]); + worst = Math.max(worst, Math.hypot(mine[0] - theirs[0], mine[1] - theirs[1])); + } +} +if (worst > 1e-6) throw new Error(`baked projection drifts ${worst}px from d3`); +console.log(`baked projection verified: worst drift ${worst.toExponential(2)}px`); + +const scale = projection.scale(); +const [tx, ty] = projection.translate(); + +// One concatenated path, not one per state: satori truncates large inline SVG +// markup, and per-state elements blew that limit and rendered a +// half-drawn map. Keep this a single path. +const d = continental.features.map((f) => geoPath(projection)(f)).filter(Boolean).join(" "); + +const out = `// GENERATED by scripts/generate-map-path.mjs. Do not edit by hand. +// Continental US outline for the daily social card, projected with +// geoAlbers().fitSize([${MAP_W}, ${MAP_H}], continentalUS). + +export const MAP_W = ${MAP_W}; +export const MAP_H = ${MAP_H}; + +/** geoAlbers scale/translate, kept for reference and for the parity test. */ +export const MAP_SCALE = ${scale}; +export const MAP_TRANSLATE: [number, number] = [${tx}, ${ty}]; + +/** + * Closed-form Albers constants, solved against d3's own output and verified to + * better than 1e-6 px across the continental US. The server projects cluster + * coordinates with these instead of importing d3-geo, which is ESM-only and + * would throw ERR_REQUIRE_ESM from the CJS server bundle on older Node. + */ +export const ALBERS = { + n: ${N}, + c: ${C}, + rho0: ${RHO0}, + lambda0Deg: ${LAMBDA0_DEG}, + k: ${k}, + dx: ${dx}, + dy: ${dy}, +} as const; + +/** ${continental.features.length} continental states + DC, one concatenated path. */ +export const US_PATH = ${JSON.stringify(d)}; +`; + +writeFileSync("server/us-map.ts", out); +console.log(`wrote server/us-map.ts states=${continental.features.length} rewound=${rewound} pathChars=${d.length} scale=${scale.toFixed(2)}`); diff --git a/scripts/preview-cards.ts b/scripts/preview-cards.ts new file mode 100644 index 0000000..c319425 --- /dev/null +++ b/scripts/preview-cards.ts @@ -0,0 +1,56 @@ +// Renders every social card template to PNG so they can be eyeballed before +// they go out on X. Nothing here posts anything. +// +// Run: npx tsx scripts/preview-cards.ts [outDir] +// Default outDir: .card-preview/ (gitignored) + +import { mkdirSync, writeFileSync } from "fs"; +import { join } from "path"; +import { ogCardForTemplate } from "../server/routes.js"; +import { renderOgPng } from "../server/og-card.js"; + +const OUT = process.argv[2] ?? ".card-preview"; + +// The weekday rotation first, then the page/off-rotation templates. +const TEMPLATES = [ + "buildout", + "gpu_rental", + "cluster_spotlight", + "grid_backlog", + "power_mix", + "tilt_status", + "top_movers", + "npi_update", + "queue_update", + "catalyst_preview", +]; + +async function main() { + mkdirSync(OUT, { recursive: true }); + let failures = 0; + + for (const t of TEMPLATES) { + try { + const card = await ogCardForTemplate(t); + const png = await renderOgPng(card); + writeFileSync(join(OUT, `${t}.png`), png); + + // A card with no as-of or no source is a bug, not a style choice. + const warn: string[] = []; + if (!card.asOf) warn.push("NO as-of"); + if (!card.source) warn.push("NO source"); + console.log( + `${t.padEnd(20)} ${String(png.length).padStart(7)} B visual=${card.visual.kind.padEnd(8)}` + + `${warn.length ? " <-- " + warn.join(", ") : ""}`, + ); + } catch (err: any) { + failures++; + console.error(`${t.padEnd(20)} FAILED: ${err?.message}`); + } + } + + console.log(`\nwrote ${TEMPLATES.length - failures}/${TEMPLATES.length} cards to ${OUT}/`); + if (failures) process.exitCode = 1; +} + +main(); diff --git a/server/__tests__/og-card.test.ts b/server/__tests__/og-card.test.ts new file mode 100644 index 0000000..8ff139f --- /dev/null +++ b/server/__tests__/og-card.test.ts @@ -0,0 +1,202 @@ +// Guards the social card. Two of these lock in bugs that shipped silently for +// months, so read the comments before relaxing an assertion. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "fs"; +import { join } from "path"; +import { buildCardTree, projectDot, assertMapPathIntact, formatAsOf, type OgCard } from "../og-card"; +import { MAP_W, MAP_H, MAP_SCALE, MAP_TRANSLATE, US_PATH } from "../us-map"; + +// ── Fonts ────────────────────────────────────────────────────────────────── +// +// The original card asked for fontWeight 700/800 and fontFamily "monospace" +// while registering only Inter-Regular. satori cannot synthesize weights, so +// every "bold" heading and every stat number silently rendered at weight 400 +// and in the wrong family. Nothing failed; the card just looked flat. If these +// files go missing the same silent regression returns. + +test("every font face the card asks for exists on disk", () => { + for (const f of ["Inter-Regular.ttf", "Inter-Bold.woff", "JetBrainsMono-Bold.woff"]) { + const p = join(process.cwd(), "server", "fonts", f); + assert.ok(existsSync(p), `missing font ${f}; card will silently render the wrong weight`); + assert.ok(readFileSync(p).length > 5_000, `${f} is suspiciously small`); + } +}); + +// ── Map geometry ─────────────────────────────────────────────────────────── +// +// satori truncates large inline SVG markup with no error. Splitting the US +// outline into per-state elements blew that limit and rendered a +// country missing 44 of its 49 states. Keep it one path, and keep it long. + +test("US outline is a single intact path", () => { + assertMapPathIntact(); + assert.ok(US_PATH.startsWith("M"), "path should start with a moveto"); + assert.ok(US_PATH.length > 30_000, `path is only ${US_PATH.length} chars; likely truncated`); +}); + +test("known cities project inside the map box, in the right places", () => { + const abilene = projectDot(32.4487, -99.7331); + const sanFrancisco = projectDot(37.7749, -122.4194); + const newYork = projectDot(40.7128, -74.006); + + for (const [name, p] of [["Abilene", abilene], ["SF", sanFrancisco], ["NYC", newYork]] as const) { + assert.ok(p, `${name} should project`); + assert.ok(p![0] >= 0 && p![0] <= MAP_W, `${name} x out of box`); + assert.ok(p![1] >= 0 && p![1] <= MAP_H, `${name} y out of box`); + } + // West coast must land left of the east coast, and Texas below New York. + assert.ok(sanFrancisco![0] < newYork![0], "SF should be west of NYC"); + assert.ok(abilene![1] > newYork![1], "Abilene should be south of NYC"); +}); + +test("coordinates outside the continental box are rejected, not clamped", () => { + assert.equal(projectDot(64.2008, -149.4937), null, "Alaska should not be plotted"); + assert.equal(projectDot(21.3099, -157.8581), null, "Hawaii should not be plotted"); +}); + +test("garbage coordinates return null instead of NaN pixels", () => { + assert.equal(projectDot(NaN, -99), null); + assert.equal(projectDot(40, Infinity), null); +}); + +// The runtime projection is d3's geoAlbers written out longhand so the CJS +// server bundle never has to require() the ESM-only d3-geo. That trade is only +// safe while the arithmetic matches d3 exactly, so pin it here. d3-geo is a +// devDependency and is used by this test and the generator script only. +test("baked projection matches d3-geo across the continental US", async () => { + const { geoAlbers } = await import("d3-geo"); + const reference = geoAlbers().scale(MAP_SCALE).translate(MAP_TRANSLATE); + + let worst = 0; + let compared = 0; + for (let lat = 25; lat <= 49; lat += 1) { + for (let lng = -124; lng <= -67; lng += 1) { + const mine = projectDot(lat, lng); + const theirs = reference([lng, lat]); + if (!mine || !theirs) continue; // box-clipped by us, not by d3 + worst = Math.max(worst, Math.hypot(mine[0] - theirs[0], mine[1] - theirs[1])); + compared++; + } + } + + assert.ok(compared > 500, `expected a dense sample, compared only ${compared} points`); + assert.ok(worst < 1e-6, `baked projection drifts ${worst}px from d3-geo`); +}); + +// ── as-of formatting ─────────────────────────────────────────────────────── + +test("formatAsOf renders a card-ready date or null", () => { + assert.equal(formatAsOf("2026-06-26"), "26 JUN 2026"); + assert.equal(formatAsOf("2026-01-05T12:00:00Z"), "5 JAN 2026"); + assert.equal(formatAsOf(null), null); + assert.equal(formatAsOf(""), null); + assert.equal(formatAsOf("not a date"), null); + assert.equal(formatAsOf("2026-13-01"), null, "month 13 has no name"); +}); + +// ── Frame ────────────────────────────────────────────────────────────────── + +/** Flatten the satori tree into the strings it will render. */ +function textsOf(node: unknown, out: string[] = []): string[] { + if (typeof node === "string") { + out.push(node); + return out; + } + if (Array.isArray(node)) { + node.forEach((n) => textsOf(n, out)); + return out; + } + if (node && typeof node === "object") { + const props = (node as { props?: { children?: unknown } }).props; + if (props && "children" in props) textsOf(props.children, out); + } + return out; +} + +const sample: OgCard = { + title: "the AI buildout, tracked", + subtitle: "every US compute cluster we count carries a public source", + stats: [{ label: "Clusters", value: "235" }], + asOf: "26 JUN 2026", + source: "235/235 entries sourced · company filings", + visual: { kind: "none" }, +}; + +test("the card always shows when it was measured and where it came from", () => { + const texts = textsOf(buildCardTree(sample)); + assert.ok(texts.includes("AS OF 26 JUN 2026"), "as-of missing from card"); + assert.ok(texts.includes(sample.source), "source line missing from card"); + assert.ok(texts.includes("GRIDTILT")); + assert.ok(texts.includes("gridtilt.com")); +}); + +test("a card with no as-of degrades honestly instead of inventing a date", () => { + const texts = textsOf(buildCardTree({ ...sample, asOf: null })); + assert.ok(texts.includes("AS OF —"), "missing as-of should print an em dash, not today"); + assert.ok( + !texts.some((t) => /\d{4}/.test(t) && t.startsWith("AS OF")), + "must not fabricate a year", + ); +}); + +test("long stat values step down in size so they cannot collide with the visual", () => { + const sizeOf = (value: string): number => { + const find = (n: any): number | null => { + if (n && typeof n === "object") { + if (n.props?.children === value && n.props?.style?.fontSize) { + return parseFloat(n.props.style.fontSize); + } + const kids = n.props?.children; + for (const k of Array.isArray(kids) ? kids : [kids]) { + const hit = find(k); + if (hit !== null) return hit; + } + } + return null; + }; + return find(buildCardTree({ ...sample, stats: [{ label: "L", value }] }))!; + }; + + assert.equal(sizeOf("235"), 44); + assert.ok(sizeOf("OpenAI / Oracle") < sizeOf("235"), "long values must shrink"); + assert.ok(sizeOf("A very long operator name") < sizeOf("OpenAI / Oracle"), "longer still must shrink further"); +}); + +test("all four visual kinds build without throwing", () => { + const kinds: OgCard["visual"][] = [ + { kind: "none" }, + { kind: "map", dots: [{ lat: 32.4487, lng: -99.7331, mw: 1200, status: "construction" }] }, + { kind: "bars", bars: [{ label: "ERCOT", value: 230, display: "230 GW", hot: true }] }, + { kind: "columns", columns: [{ label: "H100", value: 2.69, display: "$2.69" }] }, + ]; + for (const visual of kinds) { + assert.doesNotThrow(() => buildCardTree({ ...sample, visual }), `visual ${visual.kind} threw`); + } +}); + +test("bars scale against the largest value without dividing by zero", () => { + assert.doesNotThrow(() => + buildCardTree({ + ...sample, + visual: { kind: "bars", bars: [{ label: "empty", value: 0, display: "0 GW" }] }, + }), + ); +}); + +test("a highlighted map dot suppresses the status legend", () => { + const withHighlight = textsOf( + buildCardTree({ + ...sample, + visual: { + kind: "map", + dots: [ + { lat: 32.4487, lng: -99.7331, highlight: true }, + { lat: 40.7128, lng: -74.006 }, + ], + legend: true, + }, + }), + ); + assert.ok(!withHighlight.includes("operational"), "legend should be hidden when one dot is spotlit"); +}); diff --git a/server/fonts/Inter-Bold.woff b/server/fonts/Inter-Bold.woff new file mode 100644 index 0000000..c2cd54d Binary files /dev/null and b/server/fonts/Inter-Bold.woff differ diff --git a/server/fonts/JetBrainsMono-Bold.woff b/server/fonts/JetBrainsMono-Bold.woff new file mode 100644 index 0000000..d3665f5 Binary files /dev/null and b/server/fonts/JetBrainsMono-Bold.woff differ diff --git a/server/og-card.ts b/server/og-card.ts new file mode 100644 index 0000000..f98073d --- /dev/null +++ b/server/og-card.ts @@ -0,0 +1,425 @@ +// ─── Social / OG card rendering ──────────────────────────────────────────── +// +// One shared frame for every card the daily poster and /api/og produce: +// +// ┌──────────────────────────────────────────────────────────┐ +// │ ▌GRIDTILT AS OF 26 JUN 2026│ header rule +// │ title ┌──────────────┐│ +// │ subtitle │ ││ +// │ │ visual ││ +// │ STAT STAT STAT └──────────────┘│ +// │ source line gridtilt.com│ footer rule +// └──────────────────────────────────────────────────────────┘ +// +// The as-of date and the source line are permanent furniture, not decoration. +// GridTilt's claim is "tracked with sourced numbers"; a card that shows a +// number without saying when it was measured or where it came from does not +// support that claim. Callers must supply both. +// +// Layout notes learned the hard way (see server/__tests__/og-card.test.ts): +// - satori cannot synthesize weights. Every face used here is registered +// below as a real file. Asking for fontWeight 700 without registering a +// bold face silently renders regular, which is what the old card did. +// - satori truncates large inline SVG markup. The US outline must stay ONE +// concatenated path (see scripts/generate-map-path.mjs); splitting it into +// per-state elements exceeded the limit and rendered a partial map +// with no error. assertMapPathIntact() guards against a regression. + +import { readFileSync } from "fs"; +import { join } from "path"; +import { MAP_W, MAP_H, ALBERS, US_PATH } from "./us-map.js"; + +// ─── Tokens (mirror client/src/lib/tokens.ts) ────────────────────────────── + +const BRAND = "#F07800"; +const AMBER = "#F0A500"; +const BG = "#121110"; +const PANEL = "#1b1a18"; +const HAIRLINE = "#2b2926"; +const STATE_STROKE = "#45423c"; +const MUTED = "#6b7280"; +const SUBTLE = "#9ca3af"; + +const CARD_W = 1200; +const CARD_H = 630; + +// ─── Public types ────────────────────────────────────────────────────────── + +export interface OgStat { + label: string; + value: string; +} + +/** A cluster plotted on the US map. Projection happens here, not in callers. */ +export interface MapDot { + lat: number; + lng: number; + /** Planned MW; drives dot radius. Null/0 renders at the smallest size. */ + mw?: number | null; + /** operational | construction | announced; drives opacity. */ + status?: string; + /** Draws this dot larger with a ring and dims the rest. Used by the spotlight card. */ + highlight?: boolean; +} + +export interface Bar { + label: string; + /** Raw magnitude; bar widths are normalised against the largest value. */ + value: number; + /** What to print at the end of the bar, e.g. "42 GW". */ + display: string; + /** Draws the bar in brand orange instead of grey. Use for the bottleneck. */ + hot?: boolean; +} + +export interface Column { + label: string; + value: number; + display?: string; +} + +export type OgVisual = + | { kind: "map"; dots: MapDot[]; legend?: boolean } + | { kind: "bars"; bars: Bar[] } + | { kind: "columns"; columns: Column[]; caption?: string } + | { kind: "none" }; + +export interface OgCard { + title: string; + subtitle: string; + stats: OgStat[]; + /** Pre-formatted, e.g. "26 JUN 2026". Null prints "AS OF —". */ + asOf: string | null; + /** Footer-left provenance line. Required; keep it specific and true. */ + source: string; + visual: OgVisual; +} + +// ─── Map projection ──────────────────────────────────────────────────────── + +const STATUS_OPACITY: Record = { + operational: 1, + construction: 0.72, + announced: 0.38, +}; + +/** + * Project a coordinate into the card's map box, or null when it falls outside. + * + * This is d3's geoAlbers written out longhand using constants baked by + * scripts/generate-map-path.mjs. It is deliberately dependency-free: d3-geo is + * ESM-only, the server ships as a CJS bundle with deps external, and the card + * render is wrapped in a try/catch that would swallow ERR_REQUIRE_ESM into + * silently image-less tweets. The parity test in server/__tests__ pins this to + * d3's own output. + */ +export function projectDot(lat: number, lng: number): [number, number] | null { + if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null; + + const lambda = ((lng + ALBERS.lambda0Deg) * Math.PI) / 180; + const phi = (lat * Math.PI) / 180; + + const under = ALBERS.c - 2 * ALBERS.n * Math.sin(phi); + if (under < 0) return null; // beyond the projection's valid latitude range + + const rho = Math.sqrt(under) / ALBERS.n; + const theta = lambda * ALBERS.n; + const x = ALBERS.k * (rho * Math.sin(theta)) + ALBERS.dx; + const y = ALBERS.dy - ALBERS.k * (ALBERS.rho0 - rho * Math.cos(theta)); + + if (!Number.isFinite(x) || !Number.isFinite(y)) return null; + // The conic projection happily returns coordinates for the whole hemisphere, + // so anything off the card's map box (Alaska, Hawaii) is dropped, not clamped. + if (x < 0 || x > MAP_W || y < 0 || y > MAP_H) return null; + return [x, y]; +} + +function dotRadius(mw?: number | null): number { + const v = mw ?? 0; + if (v >= 1500) return 5.6; + if (v >= 600) return 4.2; + if (v >= 200) return 3.2; + return 2.5; +} + +/** + * Guard against the satori SVG truncation failure. The generated outline is + * ~36k characters; anything far below that means the file was regenerated + * wrongly or hand-edited, and the card would ship a half-drawn country. + */ +export function assertMapPathIntact(): void { + if (!US_PATH.startsWith("M") || US_PATH.length < 20_000) { + throw new Error(`US_PATH looks truncated (${US_PATH.length} chars); re-run scripts/generate-map-path.mjs`); + } +} + +function mapDataUri(): string { + assertMapPathIntact(); + const svg = + `` + + `` + + ``; + return "data:image/svg+xml;base64," + Buffer.from(svg).toString("base64"); +} + +// ─── Tiny satori node helpers ────────────────────────────────────────────── + +type Node = Record; + +const box = (style: Record, children: unknown[] = []): Node => ({ + type: "div", + props: { style: { display: "flex", ...style }, children }, +}); + +const text = (value: string, style: Record): Node => ({ + type: "div", + props: { style: { display: "flex", ...style }, children: value }, +}); + +// ─── Visual builders ─────────────────────────────────────────────────────── + +function buildMap(dots: MapDot[], legend: boolean): Node { + // When any dot is highlighted the rest recede, so the eye lands on one place. + const hasHighlight = dots.some((d) => d.highlight); + const placed = dots + .map((d) => { + const p = projectDot(d.lat, d.lng); + if (!p) return null; + const r = d.highlight ? 7 : dotRadius(d.mw); + const base = STATUS_OPACITY[d.status ?? ""] ?? 0.6; + const op = d.highlight ? 1 : hasHighlight ? 0.22 : base; + return { x: p[0], y: p[1], r, op, ring: !!d.highlight }; + }) + .filter((d): d is { x: number; y: number; r: number; op: number; ring: boolean } => d !== null) + // Draw the highlighted dot last so it sits on top of the crowd. + .sort((a, b) => Number(a.ring) - Number(b.ring)); + + const key = (opacity: number, label: string) => + box({ alignItems: "center", gap: "7px" }, [ + box({ width: "8px", height: "8px", borderRadius: "4px", backgroundColor: BRAND, opacity }), + text(label, { fontSize: "12px", color: MUTED }), + ]); + + return box({ flexDirection: "column", alignItems: "flex-end", gap: "10px" }, [ + box({ position: "relative", width: `${MAP_W}px`, height: `${MAP_H}px` }, [ + { type: "img", props: { src: mapDataUri(), width: MAP_W, height: MAP_H } }, + ...placed.flatMap((d) => [ + ...(d.ring + ? [ + box({ + position: "absolute", + left: `${d.x - d.r - 6}px`, + top: `${d.y - d.r - 6}px`, + width: `${(d.r + 6) * 2}px`, + height: `${(d.r + 6) * 2}px`, + borderRadius: `${d.r + 6}px`, + border: `2px solid ${AMBER}`, + }), + ] + : []), + box({ + position: "absolute", + left: `${d.x - d.r}px`, + top: `${d.y - d.r}px`, + width: `${d.r * 2}px`, + height: `${d.r * 2}px`, + borderRadius: `${d.r}px`, + backgroundColor: BRAND, + opacity: d.op, + }), + ]), + ]), + ...(legend && !hasHighlight + ? [box({ gap: "16px" }, [key(1, "operational"), key(0.72, "under construction"), key(0.38, "announced")])] + : []), + ]); +} + +function buildBars(bars: Bar[]): Node { + const max = Math.max(...bars.map((b) => b.value), 1); + const TRACK = 470; + return box({ flexDirection: "column", gap: "13px", width: `${TRACK}px` }, [ + ...bars.map((b) => + box({ flexDirection: "column", gap: "5px" }, [ + box({ justifyContent: "space-between", alignItems: "baseline" }, [ + text(b.label, { fontSize: "14px", color: b.hot ? "#e8e6e3" : SUBTLE }), + text(b.display, { + fontSize: "15px", + fontFamily: "JetBrains Mono", + fontWeight: 700, + color: b.hot ? AMBER : SUBTLE, + }), + ]), + box({ width: `${TRACK}px`, height: "10px", backgroundColor: "#1f1e1c", borderRadius: "2px" }, [ + box({ + width: `${Math.max(3, Math.round((b.value / max) * TRACK))}px`, + height: "10px", + backgroundColor: b.hot ? BRAND : "#4a4641", + borderRadius: "2px", + }), + ]), + ]), + ), + ]); +} + +function buildColumns(columns: Column[], caption?: string): Node { + const max = Math.max(...columns.map((c) => c.value), 1); + const min = Math.min(...columns.map((c) => c.value), 0); + const H = 200; + const span = Math.max(max - min, 1e-9); + return box({ flexDirection: "column", gap: "10px" }, [ + box({ alignItems: "flex-end", gap: "12px", height: `${H}px` }, [ + ...columns.map((c) => { + const h = Math.max(6, Math.round(((c.value - min) / span) * (H - 34)) + 12); + return box({ flexDirection: "column", alignItems: "center", gap: "6px", width: "62px" }, [ + text(c.display ?? String(c.value), { + fontSize: "13px", + fontFamily: "JetBrains Mono", + fontWeight: 700, + color: AMBER, + }), + box({ width: "34px", height: `${h}px`, backgroundColor: BRAND, borderRadius: "2px" }), + text(c.label, { fontSize: "12px", color: MUTED }), + ]); + }), + ]), + ...(caption ? [text(caption, { fontSize: "12px", color: MUTED, justifyContent: "flex-end" })] : []), + ]); +} + +function buildVisual(v: OgVisual): Node | null { + switch (v.kind) { + case "map": + return buildMap(v.dots, v.legend ?? true); + case "bars": + return buildBars(v.bars); + case "columns": + return buildColumns(v.columns, v.caption); + case "none": + return null; + } +} + +// ─── Frame ───────────────────────────────────────────────────────────────── + +/** Build the satori element tree for a card. Pure: no IO, no fonts. */ +export function buildCardTree(card: OgCard): Node { + const visual = buildVisual(card.visual); + const wide = visual === null; + + // Values are mono, so width scales with character count. Step the size down + // for long values ("OpenAI / Oracle") so the stats row cannot run into the + // visual on the right. + const statSize = (value: string) => (value.length <= 7 ? 44 : value.length <= 11 ? 34 : value.length <= 16 ? 26 : 21); + + const stat = (s: OgStat) => + box({ flexDirection: "column", gap: "7px" }, [ + text(s.label, { fontSize: "13px", color: MUTED, textTransform: "uppercase", letterSpacing: "2.5px" }), + text(s.value, { + fontSize: `${statSize(s.value)}px`, + fontFamily: "JetBrains Mono", + fontWeight: 700, + color: AMBER, + lineHeight: "1", + }), + ]); + + return box( + { + width: `${CARD_W}px`, + height: `${CARD_H}px`, + flexDirection: "column", + padding: "46px 54px", + backgroundColor: BG, + fontFamily: "Inter", + color: "#ffffff", + }, + [ + // header + box({ justifyContent: "space-between", alignItems: "center", paddingBottom: "15px", borderBottom: `1px solid ${HAIRLINE}` }, [ + box({ alignItems: "center", gap: "11px" }, [ + box({ width: "6px", height: "25px", backgroundColor: BRAND, borderRadius: "1px" }), + text("GRIDTILT", { + fontSize: "23px", + fontFamily: "JetBrains Mono", + fontWeight: 700, + color: BRAND, + letterSpacing: "3.5px", + }), + ]), + text(`AS OF ${card.asOf ?? "—"}`, { fontSize: "13px", color: MUTED, letterSpacing: "2.5px" }), + ]), + // body + box({ flexGrow: 1, paddingTop: "22px", justifyContent: "space-between" }, [ + box({ flexDirection: "column", width: wide ? "1090px" : "578px", paddingTop: "14px" }, [ + text(card.title, { fontSize: "52px", fontWeight: 700, lineHeight: "1.04", letterSpacing: "-1px" }), + text(card.subtitle, { + fontSize: "20px", + color: SUBTLE, + marginTop: "15px", + lineHeight: "1.35", + width: wide ? "900px" : "530px", + }), + box({ gap: "50px", marginTop: "auto", paddingBottom: "6px" }, card.stats.map(stat)), + ]), + // Short visuals (bars, columns) centre against the text block; the map + // is nearly full height so centring is a no-op for it. + ...(visual ? [box({ alignItems: "center", justifyContent: "flex-end" }, [visual])] : []), + ]), + // footer + box({ justifyContent: "space-between", alignItems: "center", paddingTop: "15px", borderTop: `1px solid ${HAIRLINE}`, marginTop: "16px" }, [ + text(card.source, { fontSize: "14.5px", color: MUTED }), + text("gridtilt.com", { fontSize: "15px", color: "#8a8a85", fontFamily: "JetBrains Mono", fontWeight: 700 }), + ]), + ], + ); +} + +// ─── Render ──────────────────────────────────────────────────────────────── + +interface LoadedFont { + name: string; + data: Buffer; + weight: 400 | 700; + style: "normal"; +} + +let fontCache: LoadedFont[] | null = null; + +function loadFonts(): LoadedFont[] { + if (fontCache) return fontCache; + const dir = join(process.cwd(), "server", "fonts"); + fontCache = [ + { name: "Inter", data: readFileSync(join(dir, "Inter-Regular.ttf")), weight: 400, style: "normal" }, + { name: "Inter", data: readFileSync(join(dir, "Inter-Bold.woff")), weight: 700, style: "normal" }, + { name: "JetBrains Mono", data: readFileSync(join(dir, "JetBrainsMono-Bold.woff")), weight: 700, style: "normal" }, + ]; + return fontCache; +} + +/** Render a card to a 1200x630 PNG. */ +export async function renderOgPng(card: OgCard): Promise { + const satori = (await import("satori")).default; + const { Resvg } = await import("@resvg/resvg-js"); + + const svg = await satori(buildCardTree(card) as unknown as Parameters[0], { + width: CARD_W, + height: CARD_H, + fonts: loadFonts(), + }); + + const resvg = new Resvg(svg, { fitTo: { mode: "width", value: CARD_W } }); + return Buffer.from(resvg.render().asPng()); +} + +/** "2026-06-26" -> "26 JUN 2026". Returns null for missing/unparseable input. */ +export function formatAsOf(iso: string | null | undefined): string | null { + if (!iso) return null; + const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso); + if (!m) return null; + const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]; + const mon = months[Number(m[2]) - 1]; + if (!mon) return null; + return `${Number(m[3])} ${mon} ${m[1]}`; +} diff --git a/server/routes.ts b/server/routes.ts index 1507e9d..0d6d5b0 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1055,12 +1055,53 @@ interface BacklogDataset { type QueueDataset = BacklogDataset; // ─── OG image renderer (shared by /api/og and the X media upload path) ───── +// +// Layout, fonts and the map projection live in server/og-card.ts. This file +// only gathers the data each template shows. Every card must carry an asOf +// and a source line; see the header of og-card.ts for why. + +import type { OgStat, OgCard, MapDot, Bar } from "./og-card.js"; +import { renderOgPng, formatAsOf } from "./og-card.js"; + +/** Provenance strings. Keep these specific and true; they are a public claim. */ +const SOURCE_CLUSTERS = "company filings, utility records, trade press"; +const SOURCE_QUEUE = "LBNL Queued Up + public ISO interconnection queues"; +const SOURCE_GPU = "neocloud and marketplace on-demand list prices"; +const SOURCE_MARKET = "Yahoo Finance daily closes"; + +/** Reads clusters.json once and reports how many entries carry a source. */ +function clusterSourceLine(clusters: Array<{ sources?: unknown[] }>): string { + const sourced = clusters.filter((c) => Array.isArray(c.sources) && c.sources.length > 0).length; + return `${sourced}/${clusters.length} entries sourced · ${SOURCE_CLUSTERS}`; +} + +/** + * Page OG images (link previews for /stack, /blog, ...). These are page + * furniture rather than a dataset claim, but they still show live numbers, so + * they carry the same as-of and source line as everything else. Defaults to + * today plus the market source; pass `source` when the stats come from a + * curated dataset instead. + */ +function pageCard( + title: string, + subtitle: string, + stats: OgStat[], + opts: { source?: string; visual?: OgCard["visual"] } = {}, +): OgCard { + return { + title, + subtitle, + stats, + asOf: formatAsOf(new Date().toISOString().slice(0, 10)), + source: opts.source ?? SOURCE_MARKET, + visual: opts.visual ?? { kind: "none" }, + }; +} -interface OgStat { label: string; value: string } -interface OgCard { - title: string; - subtitle: string; - stats: OgStat[]; +function clusterDots(clusters: Array): MapDot[] { + return clusters + .filter((c) => typeof c?.location?.lat === "number" && typeof c?.location?.lng === "number") + .map((c) => ({ lat: c.location.lat, lng: c.location.lng, mw: c.plannedPowerMW, status: c.status })); } async function liveIndicesStats(): Promise { @@ -1082,19 +1123,24 @@ async function liveIndicesStats(): Promise { // Per-template OG card content. Pulled from the same data sources the tweet // composers use, so the card and the tweet text stay in sync. -async function ogCardForTemplate(template: string): Promise { +/** Exported for tests and scripts/preview-cards.mjs; routes are the only caller. */ +export async function ogCardForTemplate(template: string): Promise { // ── Daily rotation cards (real data, matched to the tweet copy) ── if (template === "buildout") { const root = JSON.parse(readFileSync(join(process.cwd(), "server", "data", "clusters.json"), "utf-8")); - const m = computeClusterMetrics((root.clusters ?? []) as ClusterLite[]); + const clusters = (root.clusters ?? []) as Array; + const m = computeClusterMetrics(clusters as ClusterLite[]); return { title: "the AI buildout, tracked", - subtitle: "named US AI compute clusters", + subtitle: "every US compute cluster we count carries a public source", stats: [ { label: "Clusters", value: String(m.clusterCount) }, { label: "Planned", value: `${Math.round(m.totalPlannedMW / 1000)} GW` }, { label: "Operators", value: String(m.concentration.operatorCount) }, ], + asOf: formatAsOf(root.lastRefreshed), + source: clusterSourceLine(clusters), + visual: { kind: "map", dots: clusterDots(clusters), legend: true }, }; } if (template === "gpu_rental") { @@ -1102,14 +1148,29 @@ async function ogCardForTemplate(template: string): Promise { const g = computeGpuIndex(root.models ?? [], new Date().toISOString().slice(0, 10)); const by = Object.fromEntries(g.rows.map((r) => [r.model, r])); const px = (n?: number) => (n == null ? "n/a" : Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`); + // Price ladder across every model we track, most expensive first. + const ladder = g.rows + .filter((r) => typeof r.current === "number" && r.current > 0) + .sort((a, b) => (b.current as number) - (a.current as number)) + .slice(0, 6); return { title: "GPU rental prices", - subtitle: "on-demand, blended $/GPU-hr", + subtitle: "on-demand, blended $/GPU-hr across neoclouds and marketplaces", stats: [ { label: "H100", value: px(by.H100?.current) }, { label: "H200", value: px(by.H200?.current) }, { label: "GB200", value: px(by.GB200?.current) }, ], + asOf: formatAsOf(root.lastRefreshed), + source: SOURCE_GPU, + visual: { + kind: "columns", + columns: ladder.map((r) => ({ + label: r.model, + value: r.current as number, + display: px(r.current as number), + })), + }, }; } if (template === "cluster_spotlight") { @@ -1133,38 +1194,85 @@ async function ogCardForTemplate(template: string): Promise { { label: "Operator", value: (c.operator as string) ?? "n/a" }, { label: "Region", value: (c.gridRegion as string) ?? state ?? "n/a" }, ], + asOf: formatAsOf(root.lastRefreshed), + source: clusterSourceLine(clusters), + visual: { + kind: "map", + dots: clusterDots(clusters).map((d) => ({ + ...d, + highlight: d.lat === c.location?.lat && d.lng === c.location?.lng, + })), + }, }; } - return { title: "Compute Frontier", subtitle: "AI superclusters by GPUs and power", stats: await computeFrontierOgStats() }; + return { + title: "Compute Frontier", + subtitle: "AI superclusters by GPUs and power", + stats: await computeFrontierOgStats(), + asOf: formatAsOf(root.lastRefreshed), + source: clusterSourceLine(clusters), + visual: { kind: "map", dots: clusterDots(clusters), legend: true }, + }; } if (template === "grid_backlog") { try { const data = JSON.parse(readFileSync(join(process.cwd(), "server", "data", "interconnection-queue.json"), "utf-8")) as BacklogDataset; const h = data.headline; + const gw = (n: number) => `${n.toLocaleString("en-US")} GW`; + // The 2,290 GW national total is already the lead stat; including it here + // would flatten every regional bar into a sliver. Bars show the breakdown. + const bars: Bar[] = [ + { label: "PJM reopened cycle", value: h.pjmReopenedGW, display: gw(h.pjmReopenedGW) }, + { label: "ERCOT large-load", value: h.ercotLargeLoadGW, display: gw(h.ercotLargeLoadGW), hot: true }, + { label: "Dominion contracted", value: h.dominionContractedGW, display: gw(h.dominionContractedGW) }, + ].filter((b) => typeof b.value === "number" && b.value > 0); return { title: "the grid is the bottleneck", - subtitle: "US interconnection backlog", + subtitle: "power waiting to connect, by queue", stats: [ - { label: "Total queue", value: `${h.queueOverallGW.toLocaleString()} GW` }, + { label: "Total queue", value: gw(h.queueOverallGW) }, { label: "Median wait", value: `${h.medianWaitMonths} mo` }, - { label: "ERCOT large-load", value: `${h.ercotLargeLoadGW} GW` }, + { label: "Withdrawn", value: `${h.historicalWithdrawalPct}%` }, ], + asOf: formatAsOf(data.lastRefreshed), + source: SOURCE_QUEUE, + visual: { kind: "bars", bars }, }; } catch { - return { title: "the grid is the bottleneck", subtitle: "US interconnection backlog", stats: [] }; + return { + title: "the grid is the bottleneck", + subtitle: "US interconnection backlog", + stats: [], + asOf: null, + source: SOURCE_QUEUE, + visual: { kind: "none" }, + }; } } if (template === "power_mix") { const root = JSON.parse(readFileSync(join(process.cwd(), "server", "data", "clusters.json"), "utf-8")); - const m = computeClusterMetrics((root.clusters ?? []) as ClusterLite[]); + const clusters = (root.clusters ?? []) as Array; + const m = computeClusterMetrics(clusters as ClusterLite[]); + const ranked = m.byEnergySource.filter((b) => b.plannedMW > 0).slice(0, 5); return { title: "how the buildout gets power", - subtitle: "planned AI compute by source", + subtitle: "planned AI compute capacity by energy source", stats: m.byEnergySource.slice(0, 3).map((b) => ({ label: b.source, value: `${Math.round(b.plannedMW / 1000)} GW` })), + asOf: formatAsOf(root.lastRefreshed), + source: clusterSourceLine(clusters), + visual: { + kind: "bars", + bars: ranked.map((b, i) => ({ + label: b.source, + value: b.plannedMW, + display: `${Math.round(b.plannedMW / 1000)} GW`, + hot: i === 0, + })), + }, }; } if (template === "tilt_status") { - return { title: "today's market gauges", subtitle: "ai demand · nuclear · grid stress", stats: await liveIndicesStats() }; + return pageCard("today's market gauges", "ai demand · nuclear · grid stress", await liveIndicesStats()); } if (template === "top_movers") { const sd = await getCachedStockData("1D"); @@ -1172,23 +1280,34 @@ async function ogCardForTemplate(template: string): Promise { .filter((s) => s && typeof s.changePercent === "number") .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) .slice(0, 3); - return { - title: "today's biggest moves", - subtitle: "ai infrastructure equities", - stats: movers.map((s) => ({ label: `$${s.ticker}`, value: `${s.changePercent >= 0 ? "+" : ""}${s.changePercent.toFixed(2)}%` })), - }; + return pageCard( + "today's biggest moves", + "ai infrastructure equities", + movers.map((s) => ({ label: `$${s.ticker}`, value: `${s.changePercent >= 0 ? "+" : ""}${s.changePercent.toFixed(2)}%` })), + { + visual: { + kind: "bars", + bars: movers.map((s) => ({ + label: `$${s.ticker}`, + value: Math.abs(s.changePercent), + display: `${s.changePercent >= 0 ? "+" : ""}${s.changePercent.toFixed(2)}%`, + hot: s.changePercent >= 0, + })), + }, + }, + ); } if (template === "npi_update") { const k = await computeKpis(); - return { - title: "nuclear power index", - subtitle: `${k.npiValue.toFixed(0)} (baseline 100, jan 2024)`, - stats: [ + return pageCard( + "nuclear power index", + `${k.npiValue.toFixed(0)} (baseline 100, jan 2024)`, + [ { label: "VST", value: `${k.constituents.vstPerf >= 1 ? "+" : ""}${((k.constituents.vstPerf - 1) * 100).toFixed(0)}%` }, { label: "CEG", value: `${k.constituents.cegPerf >= 1 ? "+" : ""}${((k.constituents.cegPerf - 1) * 100).toFixed(0)}%` }, { label: "CCJ", value: `${k.constituents.ccjPerf >= 1 ? "+" : ""}${((k.constituents.ccjPerf - 1) * 100).toFixed(0)}%` }, ], - }; + ); } if (template === "queue_update") { try { @@ -1204,96 +1323,26 @@ async function ogCardForTemplate(template: string): Promise { { label: "Median wait", value: `${h.medianWaitMonths} mo` }, { label: "Withdrawal", value: `${h.historicalWithdrawalPct}%` }, ], + asOf: formatAsOf(data.lastRefreshed), + source: SOURCE_QUEUE, + visual: { kind: "none" }, }; } catch { - return { title: "us interconnection backlog", subtitle: "every named power project we can verify", stats: [] }; + return { + title: "us interconnection backlog", + subtitle: "every named power project we can verify", + stats: [], + asOf: null, + source: SOURCE_QUEUE, + visual: { kind: "none" }, + }; } } if (template === "catalyst_preview") { - return { title: "this week's catalysts", subtitle: "earnings · regulatory · policy", stats: await liveIndicesStats() }; + return pageCard("this week's catalysts", "earnings · regulatory · policy", await liveIndicesStats()); } // Fallback - return { title: "gridtilt", subtitle: "ai power infrastructure", stats: await liveIndicesStats() }; -} - -async function renderOgPng(card: OgCard): Promise { - const satori = (await import("satori")).default; - const { Resvg } = await import("@resvg/resvg-js"); - const fontData = readFileSync(join(process.cwd(), "server", "fonts", "Inter-Regular.ttf")); - - const svg = await satori( - { - type: "div", - props: { - style: { - width: "1200px", - height: "630px", - display: "flex", - flexDirection: "column", - justifyContent: "space-between", - padding: "60px", - background: "linear-gradient(135deg, #121110 0%, #1a1917 50%, #121110 100%)", - fontFamily: "Inter, sans-serif", - color: "#ffffff", - }, - children: [ - { - type: "div", - props: { - style: { display: "flex", flexDirection: "column", gap: "16px" }, - children: [ - { - type: "div", - props: { - style: { display: "flex", alignItems: "center", gap: "12px" }, - children: [ - { type: "div", props: { style: { width: "8px", height: "32px", backgroundColor: "#F07800", borderRadius: "4px" } } }, - { type: "div", props: { style: { fontSize: "28px", fontWeight: "700", color: "#F07800", letterSpacing: "2px" }, children: "GRIDTILT" } }, - ], - }, - }, - { type: "div", props: { style: { fontSize: "52px", fontWeight: "800", lineHeight: "1.1", maxWidth: "900px" }, children: card.title } }, - { type: "div", props: { style: { fontSize: "24px", color: "#9ca3af", maxWidth: "900px" }, children: card.subtitle } }, - ], - }, - }, - { - type: "div", - props: { - style: { display: "flex", justifyContent: "space-between", alignItems: "flex-end" }, - children: [ - { - type: "div", - props: { - style: { display: "flex", gap: "40px" }, - children: card.stats.map((s) => ({ - type: "div", - props: { - style: { display: "flex", flexDirection: "column", gap: "4px" }, - children: [ - { type: "div", props: { style: { fontSize: "14px", color: "#6b7280", textTransform: "uppercase", letterSpacing: "2px" }, children: s.label } }, - { type: "div", props: { style: { fontSize: "40px", fontWeight: "700", color: "#F0A500", fontFamily: "monospace" }, children: s.value } }, - ], - }, - })), - }, - }, - { type: "div", props: { style: { fontSize: "18px", color: "#6b7280" }, children: "gridtilt.com" } }, - ], - }, - }, - ], - }, - } as unknown as Parameters[0], - { - width: 1200, - height: 630, - fonts: [{ name: "Inter", data: fontData, weight: 400, style: "normal" as const }], - }, - ); - - const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } }); - return Buffer.from(resvg.render().asPng()); + return pageCard("gridtilt", "ai power infrastructure", await liveIndicesStats()); } // ─── X (Twitter) OAuth 1.0a posting client ────────────────────────────────── @@ -3057,44 +3106,44 @@ Preferred-Languages: en card = await ogCardForTemplate(template); } else if (ticker) { const companyInfo = COMPANY_DATABASE[ticker.toUpperCase()]; - card = { - title: companyInfo ? `${companyInfo.name} ($${ticker.toUpperCase()})` : `$${ticker.toUpperCase()}`, - subtitle: companyInfo ? `${companyInfo.primarySegment} Sector` : "AI Power Thesis Analysis", - stats: [{ label: "Sector", value: companyInfo?.primarySegment || "Unknown" }], - }; + card = pageCard( + companyInfo ? `${companyInfo.name} ($${ticker.toUpperCase()})` : `$${ticker.toUpperCase()}`, + companyInfo ? `${companyInfo.primarySegment} Sector` : "AI Power Thesis Analysis", + [{ label: "Sector", value: companyInfo?.primarySegment || "Unknown" }], + ); } else if (page === "stack") { - card = { title: "60+ AI Power Stocks", subtitle: "Live Data Across 8 Sectors", stats: await liveIndicesStats() }; + card = pageCard("60+ AI Power Stocks", "Live Data Across 8 Sectors", await liveIndicesStats()); } else if (page === "power-map") { - card = { title: "US AI Data Center Map", subtitle: "Filter by operator, region, and capacity (\u2265 400 MW)", stats: await liveIndicesStats() }; + card = pageCard("US AI Data Center Map", "Filter by operator, region, and capacity (\u2265 400 MW)", await liveIndicesStats()); } else if (page === "compute-frontier" && name) { - card = { title: name, subtitle: "AI Supercluster \u00b7 GridTilt", stats: await computeFrontierOgStats() }; + card = pageCard(name, "AI Supercluster \u00b7 GridTilt", await computeFrontierOgStats()); } else if (page === "compute-frontier") { - card = { title: "Compute Frontier", subtitle: "AI superclusters by GPUs and power", stats: await computeFrontierOgStats() }; + card = pageCard("Compute Frontier", "AI superclusters by GPUs and power", await computeFrontierOgStats()); } else if (page === "supply-chain") { - card = { title: "AI Power Supply Chain", subtitle: "5 systems, 20 sub-systems, silicon to substation", stats: await liveIndicesStats() }; + card = pageCard("AI Power Supply Chain", "5 systems, 20 sub-systems, silicon to substation", await liveIndicesStats()); } else if (page === "queue") { card = await ogCardForTemplate("queue_update"); } else if (page === "trade") { - card = { title: "AI Power Scenario Calculator", subtitle: "Model demand, capex, and LPT requirements through 2030", stats: await liveIndicesStats() }; + card = pageCard("AI Power Scenario Calculator", "Model demand, capex, and LPT requirements through 2030", await liveIndicesStats()); } else if (page === "portfolio") { - card = { title: "AI Power Thesis Score", subtitle: "Rate any portfolio against the AI power buildout", stats: await liveIndicesStats() }; + card = pageCard("AI Power Thesis Score", "Rate any portfolio against the AI power buildout", await liveIndicesStats()); } else if (page === "catalysts") { - card = { title: "Catalyst Calendar", subtitle: "Earnings, policy, and regulatory events for AI power", stats: await liveIndicesStats() }; + card = pageCard("Catalyst Calendar", "Earnings, policy, and regulatory events for AI power", await liveIndicesStats()); } else if (page === "blog" && name) { - card = { title: name, subtitle: "GridTilt Analysis", stats: await liveIndicesStats() }; + card = pageCard(name, "GridTilt Analysis", await liveIndicesStats()); } else if (page === "blog") { - card = { title: "GridTilt Analysis", subtitle: "Research on the AI power infrastructure thesis", stats: await liveIndicesStats() }; + card = pageCard("GridTilt Analysis", "Research on the AI power infrastructure thesis", await liveIndicesStats()); } else if (page === "subscribe") { - card = { title: "Get the Tilt", subtitle: "Weekly AI power market intel, in your inbox", stats: await liveIndicesStats() }; + card = pageCard("Get the Tilt", "Weekly AI power market intel, in your inbox", await liveIndicesStats()); } else if (page === "sector" && name) { - card = { title: `${name} Sector`, subtitle: "AI Power Infrastructure Stocks", stats: await liveIndicesStats() }; + card = pageCard(`${name} Sector`, "AI Power Infrastructure Stocks", await liveIndicesStats()); } else if (page === "region" && name) { - card = { title: `${name} Grid Region`, subtitle: "AI Data Center Locations", stats: await liveIndicesStats() }; + card = pageCard(`${name} Grid Region`, "AI Data Center Locations", await liveIndicesStats()); } else if (page === "operator" && name) { - card = { title: `${name} AI Data Centers`, subtitle: "Locations and Capacity", stats: await liveIndicesStats() }; + card = pageCard(`${name} AI Data Centers`, "Locations and Capacity", await liveIndicesStats()); } else { // home (default) - card = { title: "The grid is tilting.", subtitle: "AI power infrastructure dashboard", stats: await liveIndicesStats() }; + card = pageCard("The grid is tilting.", "AI power infrastructure dashboard", await liveIndicesStats()); } const png = await renderOgPng(card); diff --git a/server/us-map.ts b/server/us-map.ts new file mode 100644 index 0000000..e8ed458 --- /dev/null +++ b/server/us-map.ts @@ -0,0 +1,29 @@ +// GENERATED by scripts/generate-map-path.mjs. Do not edit by hand. +// Continental US outline for the daily social card, projected with +// geoAlbers().fitSize([478, 290], continentalUS). + +export const MAP_W = 478; +export const MAP_H = 290; + +/** geoAlbers scale/translate, kept for reference and for the parity test. */ +export const MAP_SCALE = 647.646481040628; +export const MAP_TRANSLATE: [number, number] = [238.83895328347967, 144.76400425965065]; + +/** + * Closed-form Albers constants, solved against d3's own output and verified to + * better than 1e-6 px across the continental US. The server projects cluster + * coordinates with these instead of importing d3-geo, which is ESM-only and + * would throw ERR_REQUIRE_ESM from the CJS server bundle on older Node. + */ +export const ALBERS = { + n: 0.6028370046288243, + c: 1.351221325417899, + rho0: 1.92825000937014, + lambda0Deg: 96, + k: 647.6464810406277, + dx: 244.0808685128568, + dy: 563.2488284924962, +} as const; + +/** 49 continental states + DC, one concatenated path. */ +export const US_PATH = "M323.299,183.383L339.329,181.961L342.016,191.522L345.925,205.573L347.368,208.604L348.607,210.277L348.324,211.438L349.489,211.992L348.05,213.6L348.266,215.016L347.656,217.027L348.985,220.316L348.647,223.358L350.053,226.321L345.329,226.921L325.114,228.937L324.929,230.45L327.289,232.42L327.086,234.307L327.913,235.167L326.629,236.964L325.333,237.453L322.728,235.811L322.225,233.052L321.458,232.805L320.734,234.983L320.584,237.049L318.041,236.697L315.925,219.481L316.353,197.764L316.64,185.218L315.59,184.114Z M127.837,156.193L118.955,220.129L99.674,217.255L89.271,211.241L66.854,197.946L68.22,195.615L70.132,195.551L70.833,194.673L70.736,192.495L69.483,192.183L69.706,187.907L71.882,186.686L72.459,185.084L72.628,182.384L74.113,180.642L75.703,180.19L77.085,178.933L75.603,176.992L74.939,173.809L73.818,171.678L74.098,170.27L74.944,168.846L75.16,166.724L74.85,164.434L75.621,157.771L78.962,157.986L79.769,159.544L80.658,159.653L81.949,157.929L83.683,148.906L114.958,154.251Z M257.817,169.759L296.682,168.313L297.606,170.448L296.308,171.905L295.001,174.112L300.845,173.746L300.687,175.885L299.387,176.596L299.2,178.36L297.623,180.337L297.943,183.071L297.163,185.12L296.279,185.487L296.891,186.451L295.485,187.41L294.944,189.38L294.014,189.934L294.294,192.169L292.659,192.886L292.751,193.63L290.925,195.604L291.551,196.82L289.966,198.715L288.663,202.278L290.378,203.688L289.606,204.665L290.242,207.003L289.601,208.596L266.602,209.283L262.528,209.377L262.404,203.333L261.062,202.86L259.229,203.455L258.239,202.411L258.419,182.372L256.535,169.779Z M20.052,74.765L26.913,76.688L37.648,79.859L45.996,82.061L41.114,101.17L37.462,115.135L45.897,127.821L54.061,140.098L60.641,149.97L65.456,157.21L74.098,170.27L73.818,171.678L74.939,173.809L75.603,176.992L77.085,178.933L75.703,180.19L74.113,180.642L72.628,182.384L72.459,185.084L71.882,186.686L69.706,187.907L69.483,192.183L70.736,192.495L70.833,194.673L70.132,195.551L68.22,195.615L55.727,194.113L45.533,192.889L44.751,191.18L45.225,188.861L45.144,185.967L44.285,183.724L42.054,180.392L39.004,177.048L38.149,177.549L36.858,176.791L37.319,175.747L36.274,173.122L34.028,173.089L30.677,170.526L30.467,169.059L28.37,166.794L25.507,166.129L23.34,164.796L20.273,164.317L19.09,162.455L20.247,159.468L19.84,158.844L20.757,156.761L18.864,154.512L19.366,152.254L18.529,151.9L17.605,149.579L16.71,148.883L16.606,147.559L14.5,142.119L13.296,140.357L13.957,136.713L14.536,137.199L15.739,135.264L15.048,132.994L13.471,132.811L11.987,130.507L11.676,128.923L12.194,127.641L11.71,125.617L12.536,122.732L14.12,123.184L14.617,118.971L13.801,119.193L13.094,121.136L11.299,121.076L9.738,118.996L10.167,116.321L9.472,113.903L8.14,112.142L7.648,110.431L6.017,106.878L6.68,106.163L6.64,102.17L7.751,100.285L7.947,97.016L6.739,93.385L5.189,91.082L5.393,88.988L8.906,84.84L9.797,83.35L9.781,81.974L11.531,78.92L11.781,75.735L11.123,74.751L12.203,72.473Z M143.503,112.184L161.911,114.352L176.033,115.615L192.899,116.869L192.171,128.274L190.085,162.527L181.54,161.877L169.625,160.976L147.116,158.694L142.212,158.054L127.837,156.193L129.659,143.003L129.687,141.744L131.061,132.153L134.032,110.996Z M433.51,83.729L443.692,81.344L443.739,81.525L445.436,88.044L445.212,89.198L444.451,89.202L441.092,90.964L436.778,91.789L435.312,93.757L433.454,94.725L431.42,96.564L430.529,95.429L432.261,93.729L431.458,92.96L429.976,84.463Z M419.82,113.07L419.332,114.585L418.615,115.509L419.186,117.239L420.797,118.614L421.726,121.29L424.177,123.756L425.044,123.691L426.278,127.45L420.719,128.548L416.887,114.669L418.05,113.138Z M408.006,125.018L409.304,125.902L408.417,127.294L407.441,125.833Z M345.329,226.921L350.053,226.321L351.751,229.436L365.02,228.528L377.46,227.674L378.284,229.998L379.436,229.766L379.521,227.433L378.844,225.4L379.608,224.408L381.95,224.998L384.639,225.027L385.967,229.78L388.032,235.042L391.984,241.75L397.563,248.674L397.007,249.328L397.801,252.826L400.295,256.478L404.516,263.951L405.539,266.336L405.916,268.891L406.355,278.215L405.441,278.555L404.913,281.563L405.399,282.413L403.784,284.79L402.922,284.436L401.158,285.591L397.988,286.594L396.854,285.596L397.035,283.832L393.982,279.286L392.063,278.652L390.609,279.556L388.974,277.012L388.304,274.816L385.832,272.638L385.093,271.07L385.073,268.649L383.856,268.393L384.336,269.752L383.344,270.27L379.29,264.7L377.816,263.354L380.18,258.472L378.256,258.997L377.147,260.585L375.533,258.57L376.411,252.215L376.017,247.092L374.663,246.033L374.056,244.432L372.129,244.325L369.567,241.927L367.656,241.059L367.328,239.416L366.021,238.964L364.768,237.255L360.733,235.275L357.563,236.233L357.935,237.935L356.833,237.758L353.034,240.335L348.767,241.326L348.734,240.083L347.555,238.719L342.14,235.943L338.382,234.902L335.11,234.857L332.428,235.368L326.629,236.964L327.913,235.167L327.086,234.307L327.289,232.42L324.929,230.45L325.114,228.937Z M362.064,178.973L360.445,181.65L360.453,182.845L363.836,184.842L364.763,184.525L366.514,186.801L366.998,188.056L368.792,190.197L371.117,191.312L372.637,193.233L375.403,194.778L375.494,196.088L377.436,197.939L380.196,199.284L381.093,201.036L381.566,203.421L382.972,204.021L384.934,206.801L385.286,208.698L387.608,209.337L385.887,213.515L385.782,215.545L385.063,217.419L385.237,219.216L384.318,220.176L384.639,225.027L381.95,224.998L379.608,224.408L378.844,225.4L379.521,227.433L379.436,229.766L378.284,229.998L377.46,227.674L365.02,228.528L351.751,229.436L350.053,226.321L348.647,223.358L348.985,220.316L347.656,217.027L348.266,215.016L348.05,213.6L349.489,211.992L348.324,211.438L348.607,210.277L347.368,208.604L345.925,205.573L342.016,191.522L339.329,181.961L351.06,180.53L357.454,179.779Z M94.66,12.277L92.261,23.481L94.027,27.06L93.435,30.077L94.603,31.705L96.033,32.505L96.037,33.323L98.258,36.857L98.347,38.133L100.122,39.682L100.061,40.425L102.291,40.801L100.392,44.707L99.668,47.272L100.138,49.129L98.501,50.067L98.825,51.329L98.202,52.468L99.636,53.948L101.847,52.801L102.846,51.732L104.041,53.094L103.655,53.966L104.02,56.241L104.881,58.735L105.678,59.703L105.238,61.828L105.974,62.911L107.591,63.333L107.976,67.124L108.777,67.899L109.787,67.008L112.249,67.506L114.241,66.844L115.231,67.581L117.18,67.409L117.468,68.086L119.146,67.926L121.35,65.899L122.29,68.063L123.537,69.458L119.103,97.231L109.899,95.769L94.482,92.964L70.123,87.822L74.686,67.534L76.541,64.129L76.097,63.075L74.629,62.553L74.412,60.912L76.89,57.337L77.785,57.156L78.962,55.704L79.063,54.647L80.254,53.515L81.151,51.682L83.48,48.771L83.177,47.121L81.536,45.935L80.901,43.83L81.291,41.89L80.719,39.736L81.048,38.861L84.307,24.187L87.43,10.69Z M288.436,100.097L303.744,99.27L311.881,98.609L311.797,100.806L313.29,103.241L314.98,107.286L317.33,134.098L316.566,136.236L317.873,138.567L318.194,140.547L317.242,142.263L317.087,143.783L315.719,146.349L314.728,146.622L315.09,148.035L314.465,148.652L314.16,151.376L314.513,152.099L313.488,153.815L314.477,155.741L310.924,157.096L310.678,158.307L311.624,159.737L310.577,160.761L307.179,159.266L306.165,159.466L304.945,161.561L305.43,162.152L304.004,162.129L301.779,159.025L302.458,158.227L301.579,156.094L301.46,154.347L298.446,152.105L297.496,152.416L296.437,150.98L293.696,148.83L293.636,147.017L294.944,143.994L294.64,142.948L295.414,141.523L294.126,140.788L292.192,140.402L291.253,141.52L290.547,140.871L289.768,137.159L286.749,134.88L283.958,132.08L282.708,128.637L282.505,126.333L283.175,124.675L283.216,122.61L285.527,121.244L285.669,119.486L286.723,118.306L286.756,116.303L285.286,114.755L285.735,112.733L288.97,111.995L291.504,110.411L291.674,108.651L292.727,107.837L292.913,105.639L292.597,104.222L290.664,103.214L290.368,102.045Z M327.704,105.481L337.544,104.379L337.632,105.122L339.277,118.629L341.008,134.468L340.404,135.043L341.481,138.063L339.969,138.303L338.475,139.545L336.185,139.301L336.53,141.527L335.155,142.623L334.738,144.115L333.268,144.84L332.759,147.787L331.829,148.639L329.749,147.787L329.279,146.516L327.504,148.141L327.773,149.37L325.842,150.001L325.156,148.937L323.049,150.27L322.437,151.52L320.065,150.044L318.943,150.522L318.1,149.781L317.449,150.654L315.243,150.971L314.513,152.099L314.16,151.376L314.465,148.652L315.09,148.035L314.728,146.622L315.719,146.349L317.087,143.783L317.242,142.263L318.194,140.547L317.873,138.567L316.566,136.236L317.33,134.098L314.98,107.286L315.87,107.958L318.43,107.722L320.772,106.183Z M281.865,89.159L283.115,89.097L283.289,90.77L284.555,91.827L283.638,93.245L284.038,95.78L284.762,97.551L287.768,98.699L288.436,100.097L290.368,102.045L290.664,103.214L292.597,104.222L292.913,105.639L292.727,107.837L291.674,108.651L291.504,110.411L288.97,111.995L285.735,112.733L285.286,114.755L286.756,116.303L286.723,118.306L285.669,119.486L285.527,121.244L283.216,122.61L283.175,124.675L282.224,124.345L280.393,122.116L279.512,122.218L267.414,122.97L255.719,123.348L246.069,123.242L245.088,121.745L245.548,118.81L244.712,116.252L244.755,113.382L243.283,112.383L243.055,110.823L243.561,109.452L243.014,107.456L241.873,106.705L240.391,101.589L238.865,99.088L239.598,97.411L239.881,95.171L240.515,94.365L239.487,93.24L239.768,91.25L239.326,90.314L240.4,90.071Z M193.438,128.354L250.033,129.906L250.887,130.962L253.674,131.808L251.774,135.141L252.873,136.254L254.28,138.926L256.141,139.463L256.452,164.092L207.545,163.396L190.085,162.527L192.171,128.274Z M349.379,137.279L351.525,138.578L352.795,137.592L356.186,138.02L356.982,136.776L358.162,136.234L358.808,138.101L359.848,138.271L361.255,139.65L361.499,143.089L362.86,145.105L364.489,146.576L365.11,147.813L366.982,148.866L368.163,148.88L365.234,152.406L362.233,154.539L362.355,155.405L361.15,156.331L361.19,157.336L359.549,158.003L359.135,159.322L354.572,161.569L354.449,161.775L346.813,162.608L340.12,163.015L338.373,163.336L328.442,163.941L317.197,165.234L315.194,164.902L315.513,166.945L304.347,167.681L303.274,167.882L303.665,166.414L305.027,166.819L305.43,162.152L304.945,161.561L306.165,159.466L307.179,159.266L310.577,160.761L311.624,159.737L310.678,158.307L310.924,157.096L314.477,155.741L313.488,153.815L314.513,152.099L315.243,150.971L317.449,150.654L318.1,149.781L318.943,150.522L320.065,150.044L322.437,151.52L323.049,150.27L325.156,148.937L325.842,150.001L327.773,149.37L327.504,148.141L329.279,146.516L329.749,147.787L331.829,148.639L332.759,147.787L333.268,144.84L334.738,144.115L335.155,142.623L336.53,141.527L336.185,139.301L338.475,139.545L339.969,138.303L341.481,138.063L340.404,135.043L341.008,134.468L344.315,134.07L346.356,136.212L346.621,137.187Z M266.602,209.283L289.601,208.596L290.544,209.857L289.899,210.389L289.913,212.696L291.386,214.054L291.771,217.399L290.76,220.069L288.595,221.797L288.143,224.371L287.188,224.168L287.227,228.335L286.074,228.513L286.863,230.715L286.214,231.553L304.437,230.534L303.728,234.313L305.483,236.685L305.979,238.517L307.231,239.613L304.453,241.352L304.311,242.479L306.714,243.065L307.608,241.203L309.764,242.854L309.704,244.287L308.568,244.987L306.332,244.581L306.67,245.613L306.026,247.27L308.004,248.564L311.049,248.785L312.248,250.373L313.129,250.557L311.702,252.583L309.946,252.336L308.351,250.463L304.669,249.654L304.545,247.739L302.809,248.47L303.017,250.07L302.301,251.602L301.019,251.929L299.947,250.32L297.679,250.391L296.967,252.166L295.481,252.744L293.803,251.782L292.5,251.726L291.118,249.008L288.85,247.88L287.998,248.106L287.029,245.796L284.466,246.22L284.351,244.799L281.886,246.267L282.251,247.305L280.355,248.373L277.323,247.99L273.777,246.56L271.286,245.955L265.984,246.652L265.295,247.102L264.411,246.006L266.622,241.926L265.822,239.774L266.487,238.58L266.13,237.037L267.058,235.835L267.991,232.892L267.77,230.475L265.024,225.943L264.915,223.458L262.768,221.016L262.528,209.377Z M449.421,67.632L448.254,67.111L448.09,65.994L446.494,65.073L442.102,50.812L439.824,43.849L442.735,41.457L441.979,40.829L442.776,38.815L443.855,37.683L443.424,37.096L444.35,35.749L443.448,33.946L443.399,30.882L444.289,29.608L443.892,26.451L447.273,16.519L448.711,16.486L449.377,18.471L450.559,18.895L452.602,17.009L454.1,16.563L454.921,15.481L457.5,16.566L459.146,17.61L462.929,29.685L463.65,32.634L466.494,32.6L466.505,33.957L467.536,34.935L467.325,36.167L468.921,37.488L470.227,36.692L472.811,40.104L471.758,42.32L470.633,41.956L470.245,43.376L469.062,43.417L469.246,44.464L467.779,44.785L466.207,47.795L465.173,46.357L464.336,46.479L465.235,48.089L463.708,49.455L462.876,48.213L462.242,49.179L460.251,49.767L459.736,48.171L458.657,48.875L459.199,49.946L458.791,52.653L459.195,53.247L458.126,54.98L456.538,54.92L456.077,56.539L454.93,57.06L454.267,58.475L452.999,58.574L452.32,57.473L451.096,59.88L451.902,60.946L450.727,61.724L450.923,62.767L449.802,64.431Z M419.352,134.79L419.163,134.83L418.927,134.88ZM385.683,120.776L416.887,114.669L420.719,128.548L426.278,127.45L425.634,132.526L424.354,133.001L422.141,134.189L420.403,135.076L420.056,133.232L419.191,132.65L420.01,131.643L418.3,130.092L418.046,130.977L416.219,131.173L415.183,129.282L415.746,129.164L415.239,126.524L415.581,125.366L414.086,121.976L414.577,119.7L415.936,119.028L415.711,116.839L414.699,117.311L414.887,118.421L412.957,120.297L412.575,121.718L413.112,124.99L412.588,126.695L413.497,129.188L414.995,130.727L415.136,132.103L416.125,133.301L415.94,134.298L413.588,132.936L410.582,132.659L409.386,131.116L407.982,132.419L407.1,131.193L408.025,129.222L408.417,127.294L409.304,125.902L408.006,125.018L407.441,125.833L406.11,125.016L404.226,124.817L403.899,123.162L402.786,122.425L401.446,122.496L399.862,119.557L398.424,119.834L396.787,119.065L396.161,120.074L394.756,120.274L394.66,121.563L392.002,121.229L390.628,123.194L389.443,123.027L388.104,125.234L386.606,126.578Z M448.231,69.955L449.063,69.923L449.891,71.763L449.965,73.421L449.086,75.208L449.496,76.905L451.318,76.67L452.849,78.126L453.051,79.558L454.031,79.677L454.466,80.916L457.018,81.376L459.337,79.627L459.185,81.289L455.702,83.681L454.3,84.13L453.178,83.337L451.93,83.998L452.097,84.793L450.717,85.681L449.563,83.859L449.245,83.556L448.195,82.997L447.065,80.522L445.873,80.837L443.739,81.525L443.692,81.344L433.51,83.729L429.976,84.463L429.697,84.081L429.876,76.343L436.413,74.912L445.785,72.867L446.386,71.611Z M348.819,103.278L337.632,105.122L337.544,104.379L327.704,105.481L320.772,106.183L322.312,104.528L323.195,101.871L324.081,100.215L324.627,97.967L324.759,94.822L324.234,91.495L321.159,85.178L321.664,82.628L320.835,79.714L322.376,76.499L322.51,73.926L322.108,72.596L323.36,71.904L323.341,70.036L325.334,69.325L326.677,67.121L326.992,71.203L327.841,71.297L328.66,69.147L328.317,65.693L328.869,64.758L330.966,63.957L329.999,61.638L331.183,59.445L332.928,59.114L335.059,60.166L337.015,60.111L338.162,61.59L339.641,61.527L342.288,62.682L343.142,62.507L344.8,64.724L343.89,66.158L345.152,67.679L345.8,69.531L345.882,73.779L344.401,75.041L344.289,77.248L342.45,78.239L341.723,80.964L342.247,81.9L344.313,82.573L345.63,80.958L347.007,77.826L349.624,76.34L351.106,77.012L352.167,78.494L353.671,83.053L354.138,85.375L355.434,88.082L355.168,92.272L353.91,93.08L353.656,91.606L352.821,92.163L352.26,95.701L350.771,97.227L350.626,99.889L348.832,102.332ZM326.75,60.261L327.029,61.599L326.023,61.96L326.244,60.006ZM311.034,69.047L309.715,67.981L310.311,66.312L308.427,66.227L309.03,64.621L308.945,62.641L307.192,61.425L306.17,60.083L302.731,59.245L301.729,59.695L298.257,58.282L290.054,56.544L289.083,54.686L287.593,54.095L290.545,52.741L291.819,51.3L295.177,50.519L297.262,48.702L298.273,48.567L299.031,47.336L301.309,45.494L302.417,43.986L304.149,42.92L305.938,43.573L303.189,47.26L302.564,48.485L302.773,50.571L304.127,48.853L306.814,48.876L308.985,49.801L311.128,52.706L312.2,53.166L314.112,52.491L314.644,53.123L316.638,53.302L320.542,50.302L322.678,49.829L325.573,49.638L327.424,48.561L328.897,48.33L329.556,51.422L331.135,51.672L332.606,50.999L333.331,51.658L334.238,50.615L336.448,50.03L336.983,53.946L338.216,55.472L339.808,55.704L339.836,54.579L341.329,54.384L342.293,55.441L341.723,56.39L337.353,56.204L335.362,56.95L332.974,55.87L332.479,57.174L332.904,58.182L331.889,58.054L330.253,56.689L327.626,56.056L326.337,56.138L325.258,57.749L323.241,58.34L320.98,58.266L320.142,58.974L320.053,60.226L317.706,61.519L317.689,60.028L316.585,59.826L316.304,61.407L314.502,61.641L313.747,62.395L312.783,65.157L310.833,68.754ZM298.921,37.476L297.107,39.028L296.118,39.284L296.118,38.114L300.706,35.122L299.934,37.029Z M275.076,53.182L274.46,52.714L272.922,53.701L273.182,60.365L272.735,61.063L270.53,62.072L268.8,64.545L268.723,66.157L269.637,66.251L270.684,67.639L269.831,69.404L270.071,71.317L269.644,75.488L271.823,77.461L273.501,77.584L274.386,78.792L276.905,79.931L277.367,81.403L279.754,83.223L281.06,83.597L282.736,86.004L282.603,87.815L283.115,89.097L281.865,89.159L240.4,90.071L240.497,69.71L238.678,68.399L237.307,66.221L239.488,63.824L239.669,62.527L239.395,58.014L238.461,56.834L237.841,54.361L237.993,51.34L237.7,50.845L237.511,43.64L236.04,39.815L235.481,37.66L235.286,33.12L235.801,31.595L234.855,28.035L250.441,28.066L250.403,23.79L251.876,23.897L252.868,24.741L253.93,30.535L254.724,31.197L257.205,31.339L257.505,31.885L260.404,32.07L260.764,33.287L263.241,32.919L263.228,32.429L265.154,31.762L266.857,31.955L268.832,32.809L269.411,33.954L270.526,33.791L271.658,36.264L272.117,35.203L274.007,34.636L274.383,35.664L276.66,36.302L276.704,37.282L277.866,38.028L280.138,37.488L281.456,36.315L283.291,35.542L284.047,37.159L285.318,36.719L286.879,36.997L288.654,36.644L290.784,37.924L292.727,37.55L292.602,38.174L290.147,39.75L286.658,41.071L284.42,42.368L281.252,45.432L279.907,47.286L277.804,49.421L274.442,52.282Z M313.134,184.313L315.59,184.114L316.64,185.218L316.353,197.764L315.925,219.481L318.041,236.697L317.017,237.276L314.658,237.211L313.645,236.541L311.342,237.209L308.188,238.739L307.231,239.613L305.979,238.517L305.483,236.685L303.728,234.313L304.437,230.534L286.214,231.553L286.863,230.715L286.074,228.513L287.227,228.335L287.188,224.168L288.143,224.371L288.595,221.797L290.76,220.069L291.771,217.399L291.386,214.054L289.913,212.696L289.899,210.389L290.544,209.857L289.601,208.596L290.242,207.003L289.606,204.665L290.378,203.688L288.663,202.278L289.966,198.715L291.551,196.82L290.925,195.604L292.751,193.63L292.659,192.886L294.294,192.169L294.014,189.934L294.944,189.38L295.485,187.41L296.891,186.451L296.279,185.487Z M279.512,122.218L280.393,122.116L282.224,124.345L283.175,124.675L282.505,126.333L282.708,128.637L283.958,132.08L286.749,134.88L289.768,137.159L290.547,140.871L291.253,141.52L292.192,140.402L294.126,140.788L295.414,141.523L294.64,142.948L294.944,143.994L293.636,147.017L293.696,148.83L296.437,150.98L297.496,152.416L298.446,152.105L301.46,154.347L301.579,156.094L302.458,158.227L301.779,159.025L304.004,162.129L305.43,162.152L305.027,166.819L303.665,166.414L303.274,167.882L302.684,167.923L302.192,167.956L302.432,170.759L300.845,173.746L295.001,174.112L296.308,171.905L297.606,170.448L296.682,168.313L257.817,169.759L256.535,169.779L256.452,164.092L256.141,139.463L254.28,138.926L252.873,136.254L251.774,135.141L253.674,131.808L250.887,130.962L250.033,129.906L247.904,126.92L246.069,123.242L255.719,123.348L267.414,122.97Z M183.725,25.538L182.689,38.244L180.823,59.736L179.96,70.413L179.83,70.402L165.081,68.941L139.999,65.891L124.433,63.571L123.537,69.458L122.29,68.063L121.35,65.899L119.146,67.926L117.468,68.086L117.18,67.409L115.231,67.581L114.241,66.844L112.249,67.506L109.787,67.008L108.777,67.899L107.976,67.124L107.591,63.333L105.974,62.911L105.238,61.828L105.678,59.703L104.881,58.735L104.02,56.241L103.655,53.966L104.041,53.094L102.846,51.732L101.847,52.801L99.636,53.948L98.202,52.468L98.825,51.329L98.501,50.067L100.138,49.129L99.668,47.272L100.392,44.707L102.291,40.801L100.061,40.425L100.122,39.682L98.347,38.133L98.258,36.857L96.037,33.323L96.033,32.505L94.603,31.705L93.435,30.077L94.027,27.06L92.261,23.481L94.66,12.277L128.186,18.683L143.399,20.957Z M183.928,93.424L197.849,94.436L223.535,95.535L223.791,96.102L227.983,98.255L228.998,97.154L230.166,97.425L234.047,97.484L238.364,99.645L238.896,101.269L240.391,101.589L241.873,106.705L243.014,107.456L243.561,109.452L243.055,110.823L243.283,112.383L244.755,113.382L244.712,116.252L245.548,118.81L245.088,121.745L246.069,123.242L247.904,126.92L250.033,129.906L193.438,128.354L192.171,128.274L192.899,116.869L176.033,115.615L177.959,92.94Z M70.123,87.822L94.482,92.964L83.683,148.906L81.949,157.929L80.658,159.653L79.769,159.544L78.962,157.986L75.621,157.771L74.85,164.434L75.16,166.724L74.944,168.846L74.098,170.27L65.456,157.21L60.641,149.97L54.061,140.098L45.897,127.821L37.462,115.135L41.114,101.17L45.996,82.061L56.528,84.766Z M439.824,43.849L442.102,50.812L446.494,65.073L448.09,65.994L448.254,67.111L449.421,67.632L449.063,69.923L448.231,69.955L446.386,71.611L445.785,72.867L436.413,74.912L435.499,74.244L435.176,72.591L435.722,71.811L435.238,70.264L434.669,65.467L435.499,62.945L435.556,60.3L436.011,59.221L435.27,56.526L437.611,54.828L438.401,52.632L437.206,51.021L437.792,48.943L437.423,47.886L437.775,44.781L439.622,44.609Z M426.242,95.991L429.362,96.925L429.128,100.38L427.94,101.429L427.61,103.301L430.083,103.62L430.566,104.854L430.962,111.05L429.372,116.104L427.955,117.777L427.063,120.929L425.86,119.279L423.197,118.915L419.648,117.138L419.103,115.53L419.004,115.296L419.332,114.585L419.82,113.07L421.87,111.656L421.818,110.772L423.972,108.366L424.163,107.298L421.281,105.579L420.876,104.198L419.748,104.066L419.372,102.806L420.207,100.572L419.269,99.566L420.827,96.786L421.002,95.466L421.902,94.43Z M142.212,158.054L147.116,158.694L169.625,160.976L181.54,161.877L181.121,167.55L180.777,167.524L178.628,195.794L177.568,207.28L176.722,218.641L143.012,215.496L142.627,216.579L143.575,217.999L127.606,216.08L126.945,221.197L118.955,220.129L127.837,156.193Z M423.186,51.492L423.828,53.761L423.899,55.914L425.11,57.728L425.409,59.891L425.047,62.344L426.47,65.196L426.257,66.208L427.956,67.715L429.552,75.397L429.876,76.343L429.697,84.081L429.976,84.463L431.458,92.96L432.261,93.729L430.529,95.429L431.42,96.564L435.156,96.623L435.713,95.78L438.729,95.034L440.166,94.288L442.235,91.897L442.738,93.122L444.179,93.333L441.583,95.734L435.821,99.739L433.267,100.872L431.47,101.174L430.284,102.033L429.128,100.38L429.362,96.925L426.242,95.991L421.902,94.43L421.362,93.721L420.091,93.945L418.186,92.391L417.997,90.707L416.728,89.649L416.116,89.849L414.886,88.524L378.851,95.604L378.364,92.779L378.332,92.594L382.78,88.522L383.306,86.845L384.719,85.513L383.752,83.727L383.02,83.48L381.981,80.505L386.463,78.356L390.655,77.626L392.385,77.608L394.428,78.477L395.476,77.762L398.961,77.126L400.907,75.903L402.725,73.429L404.155,73.065L403.556,70.014L403.923,68.094L401.887,67.254L401.983,65.774L404.739,63.221L405.549,61.332L408.516,56.808L411.641,54.139L416.993,53.224Z M378.611,158.529L384.718,157.723L391.674,156.597L423.804,150.202L425.78,154.443L423.219,154.545L422.997,155.168L420.133,156.474L419.824,157.177L417.87,157.774L418.131,158.549L420.447,157.494L420.89,157.912L423.445,156.797L424.541,157.649L426.052,156.877L427.217,159.438L426.958,160.836L425.911,161.189L424.249,164.418L421.285,165.168L421.189,167.166L422.863,168.794L423.974,168.943L422.702,172.463L420.994,172.434L418.187,173.326L416.342,174.399L413.64,177.162L411.699,180.465L411.083,184.268L409.027,183.835L405.856,185.192L393.602,176.362L383.38,177.883L383.32,176.565L381.623,174.877L380.789,175.705L380.553,174.541L369.309,175.682L366.889,176.472L365.098,177.797L362.064,178.973L357.454,179.779L351.06,180.53L350.98,177.834L352.74,177.363L353.2,175.416L355.216,173.458L357.683,173.072L359.675,171.041L361.909,170.106L363.526,167.295L364.65,166.38L365.053,167.459L368.275,164.785L369.956,164.98L370.769,162.711L372.347,161.905L372.334,159.189Z M234.855,28.035L235.801,31.595L235.286,33.12L235.481,37.66L236.04,39.815L237.511,43.64L237.7,50.845L237.993,51.34L237.841,54.361L238.461,56.834L239.395,58.014L239.669,62.527L180.823,59.736L182.689,38.244L183.725,25.538Z M372.675,96.898L375.155,111.984L374.014,112.803L374.813,113.938L375.049,115.672L374.334,118.573L374.212,122.833L371.474,127.001L370.474,127.663L369.422,127.067L368.685,128.824L367.69,128.913L366.935,131.239L367.366,132.502L366.628,133.687L365.097,132.145L363.892,135.102L364.571,136.772L363.618,137.543L363.501,139.076L361.255,139.65L359.848,138.271L358.808,138.101L358.162,136.234L356.982,136.776L356.186,138.02L352.795,137.592L351.525,138.578L349.379,137.279L346.621,137.187L346.356,136.212L344.315,134.07L341.008,134.468L339.277,118.629L337.632,105.122L348.819,103.278L352.257,104.387L353.477,105.164L354.177,104.185L356.251,105.721L357.471,106.114L361.127,104.068L363.449,104.036L365.582,101.564L368.841,98.97L372.675,96.898Z M207.545,163.396L256.452,164.092L256.535,169.779L258.419,182.372L258.239,202.411L254.642,201.214L253.708,199.915L251.3,198.815L250.696,199.818L248.298,199.77L247.785,199.149L245.595,200.278L244.675,199.655L242.685,200.215L240.841,201.957L240.129,200.956L238.193,200.135L236.152,200.119L235.501,198.803L233.125,201.334L232.378,199.89L231.3,200.311L230.497,199.364L228.32,198.454L226.661,199.92L225.978,198.347L224.658,198.132L223.923,196.868L222.155,196.326L220.959,197.356L220.221,196.401L218.387,196.474L216.386,195.415L214.503,195.478L213.918,193.273L210.986,193.043L209.857,193.374L207.871,191.109L207.158,191.204L208.089,169.112L191.794,168.27L181.121,167.55L181.54,161.877L190.085,162.527Z M33.575,29.478L34.31,29.566L35.59,31.163L35.914,32.673L35.341,36.049L38.915,38.36L42.736,37.569L44.87,37.967L47.127,39.226L47.183,40.011L51.78,39.578L52.641,40.382L54.979,40.675L57.141,40.084L60.73,39.979L63.844,40.504L65.068,40.042L80.901,43.83L81.536,45.935L83.177,47.121L83.48,48.771L81.151,51.682L80.254,53.515L79.063,54.647L78.962,55.704L77.785,57.156L76.89,57.337L74.412,60.912L74.629,62.553L76.097,63.075L76.541,64.129L74.686,67.534L70.123,87.822L56.528,84.766L45.996,82.061L37.648,79.859L26.913,76.688L20.052,74.765L12.203,72.473L11.453,70.874L11.926,67.172L12.809,64.771L12.318,62.533L13.645,60.988L15.101,58.246L17.206,55.508L18.554,52.93L22.206,43.979L22.459,42.695L24.438,38.89L26.416,33.407L26.936,30.204L27.863,28.482L31.345,27.787L32.292,29.422Z M378.364,92.779L378.851,95.604L414.886,88.524L416.116,89.849L416.728,89.649L417.997,90.707L418.186,92.391L420.091,93.945L421.362,93.721L421.902,94.43L421.002,95.466L420.827,96.786L419.269,99.566L420.207,100.572L419.372,102.806L419.748,104.066L420.876,104.198L421.281,105.579L424.163,107.298L423.972,108.366L421.818,110.772L421.87,111.656L419.82,113.07L418.05,113.138L416.887,114.669L385.683,120.776L376.846,122.278L375.155,111.984L372.675,96.898L372.675,96.898L374.106,96.029L378.332,92.594Z M449.563,83.859L450.717,85.681L449.18,86.35ZM445.873,80.837L447.065,80.522L448.195,82.997L449.245,83.556L448.222,83.635L447.81,85.486L448.148,87.849L445.212,89.198L445.436,88.044L443.739,81.525Z M365.098,177.797L366.889,176.472L369.309,175.682L380.553,174.541L380.789,175.705L381.623,174.877L383.32,176.565L383.38,177.883L393.602,176.362L405.856,185.192L404.351,186.04L402.676,188.248L401.229,191.487L401.292,193.881L400.046,196.001L397.963,196.367L397.74,197.797L395.803,199.649L394.85,201.454L393.021,202.457L391.206,204.526L391.133,205.359L389.332,206.6L387.608,209.337L385.286,208.698L384.934,206.801L382.972,204.021L381.566,203.421L381.093,201.036L380.196,199.284L377.436,197.939L375.494,196.088L375.403,194.778L372.637,193.233L371.117,191.312L368.792,190.197L366.998,188.056L366.514,186.801L364.763,184.525L363.836,184.842L360.453,182.845L360.445,181.65L362.064,178.973Z M180.823,59.736L239.669,62.527L239.488,63.824L237.307,66.221L238.678,68.399L240.497,69.71L240.4,90.071L239.326,90.314L239.768,91.25L239.487,93.24L240.515,94.365L239.881,95.171L239.598,97.411L238.865,99.088L240.391,101.589L238.896,101.269L238.364,99.645L234.047,97.484L230.166,97.425L228.998,97.154L227.983,98.255L223.791,96.102L223.535,95.535L197.849,94.436L183.928,93.424L177.959,92.94L179.83,70.402L179.96,70.413Z M315.513,166.945L315.194,164.902L317.197,165.234L328.442,163.941L338.373,163.336L340.12,163.015L346.813,162.608L354.449,161.775L354.572,161.569L372.334,159.189L372.347,161.905L370.769,162.711L369.956,164.98L368.275,164.785L365.053,167.459L364.65,166.38L363.526,167.295L361.909,170.106L359.675,171.041L357.683,173.072L355.216,173.458L353.2,175.416L352.74,177.363L350.98,177.834L351.06,180.53L339.329,181.961L323.299,183.383L315.59,184.114L313.134,184.313L296.279,185.487L297.163,185.12L297.943,183.071L297.623,180.337L299.2,178.36L299.387,176.596L300.687,175.885L300.845,173.746L302.432,170.759L302.192,167.956L302.684,167.923L303.274,167.882L304.347,167.681Z M191.794,168.27L208.089,169.112L207.158,191.204L207.871,191.109L209.857,193.374L210.986,193.043L213.918,193.273L214.503,195.478L216.386,195.415L218.387,196.474L220.221,196.401L220.959,197.356L222.155,196.326L223.923,196.868L224.658,198.132L225.978,198.347L226.661,199.92L228.32,198.454L230.497,199.364L231.3,200.311L232.378,199.89L233.125,201.334L235.501,198.803L236.152,200.119L238.193,200.135L240.129,200.956L240.841,201.957L242.685,200.215L244.675,199.655L245.595,200.278L247.785,199.149L248.298,199.77L250.696,199.818L251.3,198.815L253.708,199.915L254.642,201.214L258.239,202.411L259.229,203.455L261.062,202.86L262.404,203.333L262.528,209.377L262.768,221.016L264.915,223.458L265.024,225.943L267.77,230.475L267.991,232.892L267.058,235.835L266.13,237.037L266.487,238.58L265.822,239.774L266.622,241.926L264.411,246.006L265.295,247.102L263.684,247.199L258.602,248.84L256.759,248L256.412,246.146L255.139,247.463L254.221,247.164L253.754,248.779L254.785,249.449L254.971,251.551L253.156,253.798L250.197,256.605L244.225,259.591L243.626,259.097L241.828,259.835L241.775,259.156L239.323,259.641L238.188,258.213L237.479,258.517L240.078,261.436L238.162,262.352L236.364,261.783L236.074,263.818L233.808,265.895L231.454,269.752L229.908,273.797L228.807,273.472L228.507,274.945L229.675,274.594L229.075,277.539L228.296,277.65L228.213,279.309L229.144,280.247L229.371,283.631L230.472,284.814L230.722,286.966L231.595,288.88L228.431,290L227.164,288.506L224.761,287.909L221.561,287.963L218.86,286.055L216.793,285.815L215.27,284.297L213.162,283.741L211.757,282.281L210.925,278.87L209.164,276.777L209.448,275.063L208.686,273.187L209.021,271.596L207.82,269.763L206.781,269.538L205.141,267.869L204.675,265.813L203.275,263.901L201.211,262.266L200.33,258.825L199.394,257.854L198.222,255.075L197.898,252.83L196.735,251.161L194.705,249.631L194.273,248.617L192.383,247.646L191.018,245.088L186.763,244.277L184.183,244.239L182.044,243.229L181.483,244.371L179.1,244.58L177.163,246.802L175.814,250.49L175.216,250.508L173.701,252.628L172.074,252.567L169.779,250.651L163.922,247.373L162.868,245.79L160.635,244.163L159.268,240.805L159.427,237.894L157.996,235.395L157.816,233.322L156.881,231.925L153.317,229.645L151.577,226.85L150.042,225.756L148.543,223.351L146.261,221.921L144.913,218.773L143.575,217.999L142.627,216.579L143.012,215.496L176.722,218.641L177.568,207.28L178.628,195.794L180.777,167.524L181.121,167.55Z M109.899,95.769L119.103,97.231L117.303,108.505L134.032,110.996L131.061,132.153L129.687,141.744L129.659,143.003L127.837,156.193L114.958,154.251L83.683,148.906L94.482,92.964Z M437.423,47.886L437.792,48.943L437.206,51.021L438.401,52.632L437.611,54.828L435.27,56.526L436.011,59.221L435.556,60.3L435.499,62.945L434.669,65.467L435.238,70.264L435.722,71.811L435.176,72.591L435.499,74.244L436.413,74.912L429.876,76.343L429.552,75.397L427.956,67.715L426.257,66.208L426.47,65.196L425.047,62.344L425.409,59.891L425.11,57.728L423.899,55.914L423.828,53.761L423.186,51.492L431.234,49.618Z M424.354,133.001L425.634,132.526L424.92,134.669L423.885,135.6L423.734,138.321L422.826,142.868L421.529,144.041L420.725,142.615L420.755,139.029L422.141,134.189ZM419.163,134.83L418.927,134.88L419.352,134.79ZM395.787,121.924L400.93,124.825L401.446,122.496L402.786,122.425L403.899,123.162L404.226,124.817L406.11,125.016L407.441,125.833L408.417,127.294L408.025,129.222L407.09,129.92L406.75,131.645L407.359,132.735L409.594,131.903L410.379,133.594L413.551,133.71L414.693,134.941L417.427,135.965L416.992,139.315L418.506,141.552L417.558,142.965L417.709,144.402L418.989,145.028L418.074,146.625L415.884,145.229L415.579,145.93L417.428,146.888L421.808,146.283L423.804,150.202L391.674,156.597L384.718,157.723L378.611,158.529L372.334,159.189L354.572,161.569L359.135,159.322L359.549,158.003L361.19,157.336L361.15,156.331L362.355,155.405L362.233,154.539L365.234,152.406L368.163,148.88L368.157,149.829L369.647,151.566L371.283,152.268L372.384,152.037L373.843,150.294L375.231,151.279L377.399,150.304L381.04,147.497L381.488,148.121L382.861,146.879L382.569,144.836L383.226,142.888L384.566,140.95L384.886,138.803L386.233,136.414L386.447,133.712L388.18,134.996L389.686,135.238L390.42,134.155L391.539,129.826L392.799,130.614L395.943,125.329Z M87.43,10.69L84.307,24.187L81.048,38.861L80.719,39.736L81.291,41.89L80.901,43.83L65.068,40.042L63.844,40.504L60.73,39.979L57.141,40.084L54.979,40.675L52.641,40.382L51.78,39.578L47.183,40.011L47.127,39.226L44.87,37.967L42.736,37.569L38.915,38.36L35.341,36.049L35.914,32.673L35.59,31.163L34.31,29.566L33.575,29.478L32.292,29.422L31.345,27.787L30.113,26.967L28.808,27.283L27.659,25.902L28.394,24.512L29.609,24.042L28.779,21.34L29.547,15.196L29.176,14.246L29.638,9.883L28.688,7.853L29.075,4.502L30.525,2.64L31.686,4.09L34.259,6.352L36.311,6.907L38.19,8.044L40.265,8.081L40.909,9.293L42.648,9.735L43.171,12.443L44.107,12.52L43.355,15.756L43.053,18.805L43.868,18.718L43.847,16.025L44.74,13.659L46.652,11.52L45.974,10.243L46.398,8.45L46.234,6.3L46.996,4.988L46.99,3.204L45.907,2.638L45.194,1.095L45.811,0ZM43.947,7.492L45.041,7.297L44.255,9.432L43.332,8.272ZM42.571,3.902L43.777,2.722L44.354,4.739L43.61,6.118L42.057,5.219Z M375.155,111.984L376.846,122.278L385.683,120.776L386.606,126.578L388.104,125.234L389.443,123.027L390.628,123.194L392.002,121.229L394.66,121.563L394.756,120.274L396.161,120.074L396.787,119.065L398.424,119.834L399.862,119.557L401.446,122.496L400.93,124.825L395.787,121.924L395.943,125.329L392.799,130.614L391.539,129.826L390.42,134.155L389.686,135.238L388.18,134.996L386.447,133.712L386.233,136.414L384.886,138.803L384.566,140.95L383.226,142.888L382.569,144.836L382.861,146.879L381.488,148.121L381.04,147.497L377.399,150.304L375.231,151.279L373.843,150.294L372.384,152.037L371.283,152.268L369.647,151.566L368.157,149.829L368.163,148.88L366.982,148.866L365.11,147.813L364.489,146.576L362.86,145.105L361.499,143.089L361.255,139.65L363.501,139.076L363.618,137.543L364.571,136.772L363.892,135.102L365.097,132.145L366.628,133.687L367.366,132.502L366.935,131.239L367.69,128.913L368.685,128.824L369.422,127.067L370.474,127.663L371.474,127.001L374.212,122.833L374.334,118.573L375.049,115.672L374.813,113.938L374.014,112.803Z M287.593,54.095L289.083,54.686L290.054,56.544L298.257,58.282L301.729,59.695L302.731,59.245L306.17,60.083L307.192,61.425L308.945,62.641L309.03,64.621L308.427,66.227L310.311,66.312L309.715,67.981L311.034,69.047L310.85,70.432L309.346,70.813L308.258,73.52L307.927,75.353L308.874,75.585L310.001,74.304L311.117,71.965L312.698,70.952L313.743,67.993L315.334,67.222L315.344,68.715L314.34,70.178L312.541,75.077L312.159,77.728L312.369,79.58L311.586,80.273L311.057,82.877L311.512,85.021L310.925,86.508L310.247,90.125L310.71,92.897L311.912,95.293L311.881,98.609L303.744,99.27L288.436,100.097L287.768,98.699L284.762,97.551L284.038,95.78L283.638,93.245L284.555,91.827L283.289,90.77L283.115,89.097L282.603,87.815L282.736,86.004L281.06,83.597L279.754,83.223L277.367,81.403L276.905,79.931L274.386,78.792L273.501,77.584L271.823,77.461L269.644,75.488L270.071,71.317L269.831,69.404L270.684,67.639L269.637,66.251L268.723,66.157L268.8,64.545L270.53,62.072L272.735,61.063L273.182,60.365L272.922,53.701L274.46,52.714L275.076,53.182L276.827,53.23L282.134,51.061L284.072,49.909L284.795,50.672L283.814,52.208L286.474,53.975Z M139.999,65.891L165.081,68.941L179.83,70.402L177.959,92.94L176.033,115.615L161.911,114.352L143.503,112.184L134.032,110.996L117.303,108.505L119.103,97.231L123.537,69.458L124.433,63.571Z";