diff --git a/web/README.md b/web/README.md
index ea6fb72..4f53ed2 100644
--- a/web/README.md
+++ b/web/README.md
@@ -22,8 +22,10 @@ From the repo root, `make test-web` runs `npm test` here. It is not part of
- 12: scaffold
- 13: CelesTrak TLEs + SGP4
-- 14: Nairobi contact windows (this branch)
-- 15: Cesium globe, picker, credits, Heavens-Above/N2YO check
+- 14: Nairobi contact windows
+- 15: Cesium globe, picker, credits, Heavens-Above/N2YO check (this branch)
+
+Optional `VITE_CESIUM_ION_TOKEN` is unused in v1; the globe uses OpenStreetMap tiles so it runs without Ion.
`npm run dev` proxies `/api/celestrak` to CelesTrak so the browser can fetch
TLEs. Tests use a committed ISS fixture and never hit the network.
diff --git a/web/package-lock.json b/web/package-lock.json
index ea8e95e..a3759de 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -8,17 +8,20 @@
"name": "orbitlog-web",
"version": "0.0.1",
"dependencies": {
+ "cesium": "1.125.0",
"react": "19.3.1",
"react-dom": "19.3.1",
"satellite.js": "7.1.0"
},
"devDependencies": {
+ "@types/node": "22.18.0",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@vitejs/plugin-react": "4.7.0",
"jsdom": "26.1.0",
"typescript": "5.9.3",
"vite": "6.4.4",
+ "vite-plugin-cesium": "1.2.22",
"vitest": "3.2.7"
},
"engines": {
@@ -328,6 +331,79 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@cesium/engine": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-13.1.0.tgz",
+ "integrity": "sha512-1PX7sMhtgBgaSOtyQ0lTPvG35bMfR1ngtgBZqzNnR67lp+tVv9lNyAg/byBSnijL2b2NJNC2Iec6A6Z/Rj12OA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@tweenjs/tween.js": "^25.0.0",
+ "@zip.js/zip.js": "^2.7.34",
+ "autolinker": "^4.0.0",
+ "bitmap-sdf": "^1.0.3",
+ "dompurify": "^3.0.2",
+ "draco3d": "^1.5.1",
+ "earcut": "^3.0.0",
+ "grapheme-splitter": "^1.0.4",
+ "jsep": "^1.3.8",
+ "kdbush": "^4.0.1",
+ "ktx-parse": "^0.7.0",
+ "lerc": "^2.0.0",
+ "mersenne-twister": "^1.1.0",
+ "meshoptimizer": "^0.22.0",
+ "pako": "^2.0.4",
+ "protobufjs": "^7.1.0",
+ "rbush": "3.0.1",
+ "topojson-client": "^3.1.0",
+ "urijs": "^1.19.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@cesium/widgets": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/@cesium/widgets/-/widgets-10.2.0.tgz",
+ "integrity": "sha512-f0Wrp3MG02P2KSAopVESHWOhF+2eK9cQR9prEYGWuPM3iF6YktKrZqXzNnsOxCw1KPup0aSXsCrFl2asT5jF9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@cesium/engine": "^14.0.0",
+ "nosleep.js": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@cesium/widgets/node_modules/@cesium/engine": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-14.0.0.tgz",
+ "integrity": "sha512-nmW0uQCyg4CRqi3a8o30gU9S9bFm3TLh2fRO74iv+6a8FFArvZ1xF7IqG2tyRjSXkyH4c5vpavNRT3R51R8NyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@tweenjs/tween.js": "^25.0.0",
+ "@zip.js/zip.js": "^2.7.34",
+ "autolinker": "^4.0.0",
+ "bitmap-sdf": "^1.0.3",
+ "dompurify": "^3.0.2",
+ "draco3d": "^1.5.1",
+ "earcut": "^3.0.0",
+ "grapheme-splitter": "^1.0.4",
+ "jsep": "^1.3.8",
+ "kdbush": "^4.0.1",
+ "ktx-parse": "^0.7.0",
+ "lerc": "^2.0.0",
+ "mersenne-twister": "^1.1.0",
+ "meshoptimizer": "^0.22.0",
+ "pako": "^2.0.4",
+ "protobufjs": "^7.1.0",
+ "rbush": "3.0.1",
+ "topojson-client": "^3.1.0",
+ "urijs": "^1.19.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@csstools/color-helpers": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
@@ -955,6 +1031,63 @@
"node": "^22.20 || ^24.12 || >=25"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
+ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -962,6 +1095,40 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@rollup/pluginutils": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
+ "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "estree-walker": "^2.0.1",
+ "picomatch": "^2.2.2"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/pluginutils/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.64.10",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.10.tgz",
@@ -1351,6 +1518,12 @@
"win32"
]
},
+ "node_modules/@tweenjs/tween.js": {
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
+ "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
+ "license": "MIT"
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1421,6 +1594,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "22.18.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz",
+ "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
"node_modules/@types/react": {
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
@@ -1441,6 +1623,13 @@
"@types/react": "^19.3.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -1577,6 +1766,17 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@zip.js/zip.js": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.24.0.tgz",
+ "integrity": "sha512-WlaVKzdqBK+RF3bdiXZWbdCnvGB6PdO4IHC1mn/2Yfl3bASgqUNegnU3+PNg61iecwoPfUi5BgLUPqBukXpyWw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "bun": ">=0.7.0",
+ "deno": ">=1.0.0",
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -1597,6 +1797,28 @@
"node": ">=12"
}
},
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/autolinker": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-4.1.5.tgz",
+ "integrity": "sha512-vEfYZPmvVOIuE567XBVCsx8SBgOYtjB2+S1iAaJ+HgH+DNjAcrHem2hmAeC9yaNGWayicv4yR+9UaJlkF3pvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "pnpm": ">=10.10.0"
+ }
+ },
"node_modules/baseline-browser-mapping": {
"version": "2.11.30",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.30.tgz",
@@ -1610,6 +1832,12 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bitmap-sdf": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz",
+ "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==",
+ "license": "MIT"
+ },
"node_modules/browserslist": {
"version": "4.29.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz",
@@ -1675,6 +1903,23 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/cesium": {
+ "version": "1.125.0",
+ "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.125.0.tgz",
+ "integrity": "sha512-T6V1xKFQOxydJJtG6ammyDOwKMerkGLdxQJLJSLHhhh6VKqpUyp+GXSKjnzlAmpLDjQLF1UkhbJ98WC59am/fA==",
+ "license": "Apache-2.0",
+ "workspaces": [
+ "packages/engine",
+ "packages/widgets"
+ ],
+ "dependencies": {
+ "@cesium/engine": "^13.1.0",
+ "@cesium/widgets": "^10.1.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
@@ -1702,6 +1947,12 @@
"node": ">= 16"
}
},
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "license": "MIT"
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1779,6 +2030,55 @@
"node": ">=6"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dompurify": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz",
+ "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/draco3d": {
+ "version": "1.5.9",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.9.tgz",
+ "integrity": "sha512-MqC0eAfW5gPATGY3GABIyFWpHwdDGAyewTdek5R/e+mhI9ZDUMKb2mTDkTMUW0+Vm7an+ThEMUop3+svS72Uug==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/earcut": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.4.tgz",
+ "integrity": "sha512-FHc6yNpnTORB0tM5kmZ03iD2mDuNakDE2lvf4Hw1XkfxtEHIGVi01B/GqUSwx+NcC1WGjidAfpTXzeALtPtfpg==",
+ "license": "ISC"
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.459",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.459.tgz",
@@ -1786,6 +2086,16 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -1858,6 +2168,13 @@
"node": ">=6"
}
},
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -1868,6 +2185,16 @@
"@types/estree": "^1.0.0"
}
},
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
@@ -1896,6 +2223,32 @@
}
}
},
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1921,6 +2274,19 @@
"node": ">=6.9.0"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/grapheme-splitter": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "license": "MIT"
+ },
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -1934,6 +2300,27 @@
"node": ">=18"
}
},
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -1975,6 +2362,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -1982,6 +2376,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/is-reference": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2029,6 +2433,15 @@
}
}
},
+ "node_modules/jsep": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.6.0.tgz",
+ "integrity": "sha512-Y8Tr54CX7B5YGAwo7/n8eumidEX3Opx98RyyQVnlHqUaZcPA8uUzL2v1rshr9ypW9Q6hNjfJtg3sL+8kaNE7TA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.16.0"
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -2055,6 +2468,43 @@
"node": ">=6"
}
},
+ "node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/kdbush": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
+ "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
+ "license": "ISC"
+ },
+ "node_modules/ktx-parse": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.7.1.tgz",
+ "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==",
+ "license": "MIT"
+ },
+ "node_modules/lerc": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lerc/-/lerc-2.0.0.tgz",
+ "integrity": "sha512-7qo1Mq8ZNmaR4USHHm615nEW2lPeeWJ3bTyoqFbd35DLx0LUH7C6ptt5FDCTAlbIzs3+WKrk5SkJvw8AFDE2hg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
@@ -2082,6 +2532,31 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/mersenne-twister": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz",
+ "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==",
+ "license": "MIT"
+ },
+ "node_modules/meshoptimizer": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
+ "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
+ "license": "MIT"
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2118,6 +2593,12 @@
"node": ">=18"
}
},
+ "node_modules/nosleep.js": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz",
+ "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==",
+ "license": "MIT"
+ },
"node_modules/nwsapi": {
"version": "2.2.28",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz",
@@ -2125,6 +2606,35 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/pako": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
+ "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -2138,6 +2648,16 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -2204,6 +2724,29 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/protobufjs": {
+ "version": "7.6.6",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz",
+ "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.1",
+ "@protobufjs/fetch": "^1.1.1",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.3.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2214,6 +2757,31 @@
"node": ">=6"
}
},
+ "node_modules/quickselect": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
+ "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
+ "license": "ISC"
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/rbush": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz",
+ "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==",
+ "license": "MIT",
+ "dependencies": {
+ "quickselect": "^2.0.0"
+ }
+ },
"node_modules/react": {
"version": "19.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz",
@@ -2340,6 +2908,71 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -2357,6 +2990,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "deprecated": "Please use @jridgewell/sourcemap-codec instead",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -2364,6 +3005,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/std-env": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
@@ -2479,6 +3130,30 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/topojson-client": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
+ "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "2"
+ },
+ "bin": {
+ "topo2geo": "bin/topo2geo",
+ "topomerge": "bin/topomerge",
+ "topoquantize": "bin/topoquantize"
+ }
+ },
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
@@ -2505,6 +3180,12 @@
"node": ">=18"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -2519,6 +3200,22 @@
"node": ">=14.17"
}
},
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz",
@@ -2550,6 +3247,12 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/urijs": {
+ "version": "1.19.11",
+ "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz",
+ "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
+ "license": "MIT"
+ },
"node_modules/vite": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz",
@@ -2648,6 +3351,72 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/vite-plugin-cesium": {
+ "version": "1.2.22",
+ "resolved": "https://registry.npmjs.org/vite-plugin-cesium/-/vite-plugin-cesium-1.2.22.tgz",
+ "integrity": "sha512-OnS+VKNGck4kUu4/67Fdfhz0/zF9mDVNUp9hUWtX19C38O0mJsJy2MH1ev2QcrVLf6VieJ7vCGxkLchdB1n1HQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fs-extra": "^9.1.0",
+ "rollup-plugin-external-globals": "^0.6.1",
+ "serve-static": "^1.14.1"
+ },
+ "peerDependencies": {
+ "cesium": "^1.95.0",
+ "vite": ">=2.7.1"
+ }
+ },
+ "node_modules/vite-plugin-cesium/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite-plugin-cesium/node_modules/magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ },
+ "node_modules/vite-plugin-cesium/node_modules/rollup": {
+ "version": "2.80.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
+ "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/vite-plugin-cesium/node_modules/rollup-plugin-external-globals": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-external-globals/-/rollup-plugin-external-globals-0.6.1.tgz",
+ "integrity": "sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^4.0.0",
+ "estree-walker": "^2.0.1",
+ "is-reference": "^1.2.1",
+ "magic-string": "^0.25.7"
+ },
+ "peerDependencies": {
+ "rollup": "^2.25.0"
+ }
+ },
"node_modules/vitest": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
diff --git a/web/package.json b/web/package.json
index dbd1491..8a18042 100644
--- a/web/package.json
+++ b/web/package.json
@@ -13,17 +13,20 @@
"test": "vitest run"
},
"dependencies": {
+ "cesium": "1.125.0",
"react": "19.3.1",
"react-dom": "19.3.1",
"satellite.js": "7.1.0"
},
"devDependencies": {
+ "@types/node": "22.18.0",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@vitejs/plugin-react": "4.7.0",
"jsdom": "26.1.0",
"typescript": "5.9.3",
"vite": "6.4.4",
+ "vite-plugin-cesium": "1.2.22",
"vitest": "3.2.7"
}
}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 7ff96d4..1a19afe 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react";
+import Globe from "./Globe";
import { tleCache, type CachedTle } from "./orbit/cache";
import { SATELLITES } from "./orbit/catalog";
import { countdownSeconds, formatCountdown } from "./orbit/countdown";
@@ -28,6 +29,9 @@ export default function App() {
}
}
setCached(next);
+ if (!next[SATELLITES[0].catnr]) {
+ void load(SATELLITES[0].catnr);
+ }
}, []);
useEffect(() => {
@@ -50,6 +54,7 @@ export default function App() {
}
const tle = cached[selected];
+ const sat = SATELLITES.find((s) => s.catnr === selected) ?? SATELLITES[0];
const passBucket = Math.floor(now.getTime() / 15_000);
const pass: Pass | null = useMemo(() => {
if (!tle) {
@@ -64,79 +69,78 @@ export default function App() {
now.getTime() < pass.set.getTime();
return (
-
- OrbitLog
- Ride along with a real satellite and watch it think.
-
- A contact window is the few minutes a satellite is
- above {DEFAULT_ELEVATION_MASK_DEG}° elevation at {NAIROBI.name}.{" "}
- Look angles are azimuth (compass direction) and
- elevation (angle above the horizon). The globe comes next.
-
-
- {SATELLITES.map((sat) => {
- const hit = cached[sat.catnr];
- const active = sat.catnr === selected;
- return (
-
- setSelected(sat.catnr)}
- >
- {sat.name} CATNR {sat.catnr}
-
-
- {hit ? `TLE fetched ${hit.fetchedAt}` : "TLE not fetched"}
-
- void load(sat.catnr)}
- >
- {busy === sat.catnr ? "Fetching…" : "Fetch TLE"}
-
-
- );
- })}
-
- {error ? {error}
: null}
-
-
- Next pass over {NAIROBI.name} (mask {DEFAULT_ELEVATION_MASK_DEG}°)
-
- {!tle ? (
- Fetch a TLE to predict the next window.
- ) : !pass ? (
- No pass in the next 36 hours for this TLE.
- ) : (
-
- Countdown
-
- {inPass
- ? `In contact · sets in ${formatCountdown(countdownSeconds(pass.set, now))}`
- : formatCountdown(countdownSeconds(pass.rise, now))}
-
- Rise
- {utc(pass.rise)}
- Max elevation
-
- {pass.maxElDeg.toFixed(1)}° at {utc(pass.maxElAt)}
-
- Set
- {utc(pass.set)}
-
- )}
-
-
- v2, the onboard diagnosis story, is {V2_STATUS} until
- the benchmark writes files under results/. This page will
- never invent those numbers.
-
-
- Faults in OrbitLog are simulated on a ground Linux box, not live
- satellite telemetry. TLE data: CelesTrak.
-
-
+
+
+
+ OrbitLog
+ Ride along with a real satellite.
+
+ A contact window is the few minutes a satellite is
+ above {DEFAULT_ELEVATION_MASK_DEG}° elevation at {NAIROBI.name}. The
+ yellow arc is that window. Diagnosis UI is{" "}
+ {V2_STATUS} .
+
+
+ {SATELLITES.map((item) => {
+ const hit = cached[item.catnr];
+ const active = item.catnr === selected;
+ return (
+
+ setSelected(item.catnr)}
+ >
+ {item.name} {item.catnr}
+
+
+ {hit ? `TLE ${hit.fetchedAt}` : "TLE not fetched"}
+
+ void load(item.catnr)}
+ >
+ {busy === item.catnr ? "Fetching…" : "Fetch TLE"}
+
+
+ );
+ })}
+
+ {error ? {error}
: null}
+
+
+ Next pass · {NAIROBI.name} · mask {DEFAULT_ELEVATION_MASK_DEG}°
+
+ {!tle ? (
+ Fetching a TLE…
+ ) : !pass ? (
+ No pass in the next 36 hours for this TLE.
+ ) : (
+
+ Countdown
+
+ {inPass
+ ? `In contact · sets in ${formatCountdown(countdownSeconds(pass.set, now))}`
+ : formatCountdown(countdownSeconds(pass.rise, now))}
+
+ Rise
+ {utc(pass.rise)}
+ Max elevation
+
+ {pass.maxElDeg.toFixed(1)}° at {utc(pass.maxElAt)}
+
+ Set
+ {utc(pass.set)}
+
+ )}
+
+
+ Simulated ground faults, not live telemetry. Map tiles: OpenStreetMap.
+ Orbits: CelesTrak TLEs + SGP4 (satellite.js). Globe: CesiumJS. Inspired
+ by Skyhitch / God's Eye View (MIT), not a copy of their code.
+
+
+
);
}
diff --git a/web/src/Globe.tsx b/web/src/Globe.tsx
new file mode 100644
index 0000000..0594ebf
--- /dev/null
+++ b/web/src/Globe.tsx
@@ -0,0 +1,145 @@
+import {
+ Cartesian2,
+ Cartesian3,
+ Color,
+ ConstantPositionProperty,
+ EllipsoidTerrainProvider,
+ ImageryLayer,
+ OpenStreetMapImageryProvider,
+ Viewer,
+} from "cesium";
+import { useEffect, useRef } from "react";
+import { llaAt, sampleTrack } from "./orbit/geo";
+import type { Pass } from "./orbit/pass";
+import { NAIROBI } from "./orbit/station";
+import type { ParsedTle } from "./orbit/tle";
+
+type Props = {
+ name: string;
+ tle: ParsedTle | null;
+ pass: Pass | null;
+};
+
+const ORBIT_MS = 92 * 60 * 1000;
+const ORBIT_STEP_MS = 30_000;
+const PASS_STEP_MS = 5_000;
+
+function cart(lla: { latitudeDeg: number; longitudeDeg: number; heightKm: number }) {
+ return Cartesian3.fromDegrees(
+ lla.longitudeDeg,
+ lla.latitudeDeg,
+ lla.heightKm * 1000,
+ );
+}
+
+export default function Globe({ name, tle, pass }: Props) {
+ const host = useRef(null);
+ const viewerRef = useRef(null);
+
+ useEffect(() => {
+ const el = host.current;
+ if (!el) {
+ return;
+ }
+ const viewer = new Viewer(el, {
+ baseLayer: new ImageryLayer(
+ new OpenStreetMapImageryProvider({
+ url: "https://tile.openstreetmap.org/",
+ }),
+ ),
+ baseLayerPicker: false,
+ geocoder: false,
+ homeButton: false,
+ infoBox: false,
+ sceneModePicker: false,
+ selectionIndicator: false,
+ timeline: false,
+ animation: false,
+ navigationHelpButton: false,
+ terrainProvider: new EllipsoidTerrainProvider(),
+ });
+ viewer.scene.globe.enableLighting = true;
+ viewer.entities.add({
+ name: "Nairobi",
+ position: Cartesian3.fromDegrees(
+ NAIROBI.longitudeDeg,
+ NAIROBI.latitudeDeg,
+ 0,
+ ),
+ point: { pixelSize: 10, color: Color.WHITE },
+ label: {
+ text: "Nairobi",
+ font: "14px sans-serif",
+ fillColor: Color.WHITE,
+ pixelOffset: new Cartesian2(0, -16),
+ showBackground: true,
+ },
+ });
+ viewerRef.current = viewer;
+ return () => {
+ viewer.destroy();
+ viewerRef.current = null;
+ };
+ }, []);
+
+ useEffect(() => {
+ const viewer = viewerRef.current;
+ if (!viewer || viewer.isDestroyed()) {
+ return;
+ }
+ const extras = viewer.entities.values.filter((e) => e.name !== "Nairobi");
+ for (const entity of extras) {
+ viewer.entities.remove(entity);
+ }
+ if (!tle) {
+ return;
+ }
+ const now = new Date();
+ const orbit = sampleTrack(tle, new Date(now.getTime() - 10 * 60_000), ORBIT_MS, ORBIT_STEP_MS);
+ viewer.entities.add({
+ name: "orbit",
+ polyline: {
+ positions: orbit.map(cart),
+ width: 2,
+ material: Color.CYAN.withAlpha(0.55),
+ },
+ });
+ if (pass) {
+ const windowTrack = sampleTrack(
+ tle,
+ pass.rise,
+ pass.set.getTime() - pass.rise.getTime(),
+ PASS_STEP_MS,
+ );
+ viewer.entities.add({
+ name: "contact",
+ polyline: {
+ positions: windowTrack.map(cart),
+ width: 4,
+ material: Color.YELLOW,
+ },
+ });
+ }
+ const sat = viewer.entities.add({
+ name: "sat",
+ position: cart(llaAt(tle, now)),
+ point: { pixelSize: 12, color: Color.YELLOW },
+ label: {
+ text: name,
+ font: "13px sans-serif",
+ fillColor: Color.YELLOW,
+ pixelOffset: new Cartesian2(0, -16),
+ showBackground: true,
+ },
+ });
+ const id = window.setInterval(() => {
+ if (viewer.isDestroyed()) {
+ return;
+ }
+ sat.position = new ConstantPositionProperty(cart(llaAt(tle, new Date())));
+ }, 1000);
+ return () => window.clearInterval(id);
+ }, [tle, pass, name]);
+
+ return
;
+}
diff --git a/web/src/index.css b/web/src/index.css
index e0e1a4e..cb5db9c 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -9,14 +9,38 @@
box-sizing: border-box;
}
+html,
+body,
+#root {
+ height: 100%;
+}
+
body {
margin: 0;
}
-main {
- max-width: 40rem;
- margin: 0 auto;
- padding: 3rem 1.25rem;
+.shell {
+ position: relative;
+ height: 100%;
+ overflow: hidden;
+}
+
+.globe {
+ position: absolute;
+ inset: 0;
+}
+
+.hud {
+ position: absolute;
+ z-index: 1;
+ top: 1rem;
+ left: 1rem;
+ width: min(24rem, calc(100% - 2rem));
+ max-height: calc(100% - 2rem);
+ overflow: auto;
+ padding: 1rem 1.1rem;
+ background: #111318f2;
+ border: 1px solid #2a2e35;
}
.kicker {
@@ -28,7 +52,7 @@ main {
}
h1 {
- font-size: 1.75rem;
+ font-size: 1.35rem;
font-weight: 600;
line-height: 1.25;
margin: 0 0 1rem;
diff --git a/web/src/orbit/geo.test.ts b/web/src/orbit/geo.test.ts
new file mode 100644
index 0000000..8ba1892
--- /dev/null
+++ b/web/src/orbit/geo.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from "vitest";
+import issTle from "./fixtures/iss.tle?raw";
+import { llaAt } from "./geo";
+import { parseTle } from "./tle";
+
+describe("llaAt", () => {
+ it("keeps ISS within its 51.6° inclination band", () => {
+ const tle = parseTle(issTle);
+ const when = new Date(Date.UTC(2026, 9, 19, 10, 26, 22));
+ const lla = llaAt(tle, when);
+ expect(Math.abs(lla.latitudeDeg)).toBeLessThan(52);
+ expect(lla.longitudeDeg).toBeGreaterThanOrEqual(-180);
+ expect(lla.longitudeDeg).toBeLessThanOrEqual(180);
+ expect(lla.heightKm).toBeGreaterThan(300);
+ expect(lla.heightKm).toBeLessThan(500);
+ });
+});
diff --git a/web/src/orbit/geo.ts b/web/src/orbit/geo.ts
new file mode 100644
index 0000000..9eccf16
--- /dev/null
+++ b/web/src/orbit/geo.ts
@@ -0,0 +1,47 @@
+/**
+ * Convert SGP4’s ECI kilometres into geodetic lat/lon/height so Cesium can
+ * place the satellite on the globe.
+ */
+import * as satellite from "satellite.js";
+import { satrecFromTle } from "./look";
+import { propagateEci } from "./sgp4";
+import type { ParsedTle } from "./tle";
+
+export type Lla = {
+ latitudeDeg: number;
+ longitudeDeg: number;
+ heightKm: number;
+};
+
+export function eciToLla(eci: { x: number; y: number; z: number }, when: Date): Lla {
+ const gd = satellite.eciToGeodetic(eci, satellite.gstime(when));
+ return {
+ latitudeDeg: satellite.degreesLat(gd.latitude),
+ longitudeDeg: satellite.degreesLong(gd.longitude),
+ heightKm: gd.height,
+ };
+}
+
+export function llaAt(tle: ParsedTle, when: Date): Lla {
+ return eciToLla(propagateEci(tle, when), when);
+}
+
+export function sampleTrack(
+ tle: ParsedTle,
+ from: Date,
+ durationMs: number,
+ stepMs: number,
+): Lla[] {
+ const satrec = satrecFromTle(tle);
+ const out: Lla[] = [];
+ for (let t = from.getTime(); t <= from.getTime() + durationMs; t += stepMs) {
+ const when = new Date(t);
+ const result = satellite.propagate(satrec, when);
+ const position = result?.position;
+ if (!position || typeof position.x !== "number") {
+ continue;
+ }
+ out.push(eciToLla(position, when));
+ }
+ return out;
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
index eed018f..5ffb14a 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -1,8 +1,66 @@
import react from "@vitejs/plugin-react";
+import cesium from "vite-plugin-cesium";
import { defineConfig } from "vitest/config";
+function cesiumDevGlobal() {
+ let serve = false;
+ return {
+ name: "cesium-dev-global",
+ enforce: "pre" as const,
+ config(_cfg: unknown, env: { command: string }) {
+ serve = env.command === "serve";
+ return {
+ optimizeDeps: {
+ exclude: ["cesium", "satellite.js"],
+ },
+ };
+ },
+ resolveId(id: string) {
+ if (serve && id === "cesium") {
+ return "\0cesium-global";
+ }
+ return undefined;
+ },
+ load(id: string) {
+ if (id === "\0cesium-global") {
+ return `
+ const c = window.Cesium;
+ export const Cartesian2 = c.Cartesian2;
+ export const Cartesian3 = c.Cartesian3;
+ export const Color = c.Color;
+ export const ConstantPositionProperty = c.ConstantPositionProperty;
+ export const EllipsoidTerrainProvider = c.EllipsoidTerrainProvider;
+ export const ImageryLayer = c.ImageryLayer;
+ export const OpenStreetMapImageryProvider = c.OpenStreetMapImageryProvider;
+ export const Viewer = c.Viewer;
+ `;
+ }
+ const normalized = id.replaceAll("\\", "/");
+ if (normalized.includes("satellite.js/wasm-build")) {
+ return "export default {}";
+ }
+ return undefined;
+ },
+ transformIndexHtml() {
+ if (!serve) {
+ return [];
+ }
+ return [
+ {
+ tag: "script",
+ attrs: { src: "/cesium/Cesium.js" },
+ },
+ ];
+ },
+ };
+}
+
export default defineConfig({
- plugins: [react()],
+ plugins: [
+ react(),
+ cesium(),
+ cesiumDevGlobal(),
+ ],
server: {
proxy: {
"/api/celestrak": {