@@ -525,7 +535,7 @@ onUnmounted(() => {
:currentAltitude="currentAltitude"
:unit="altitudeUnit.label"
start-time=" "
- :end-time="formatDate('HH:MM A', latest.timestamp)"
+ :end-time="formatDate('HH:mm:ss', latest.timestamp)"
color="#ffaa00"
/>
diff --git a/server/package-lock.json b/server/package-lock.json
index a9eedb4..baf0a8c 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -15,9 +15,568 @@
"cheerio": "^1.2.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
+ "sharp": "^0.35.4",
"yaml": "^2.9.0"
}
},
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz",
+ "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz",
+ "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz",
+ "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.4"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz",
+ "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz",
+ "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz",
+ "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz",
+ "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz",
+ "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz",
+ "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz",
+ "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz",
+ "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz",
+ "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz",
+ "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz",
+ "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz",
+ "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz",
+ "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz",
+ "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz",
+ "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz",
+ "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz",
+ "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz",
+ "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
+ "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz",
+ "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.4"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz",
+ "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz",
+ "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz",
+ "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@scalar/client-side-rendering": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@scalar/client-side-rendering/-/client-side-rendering-0.2.6.tgz",
@@ -90,24 +649,6 @@
"node": ">=20"
}
},
- "node_modules/@types/node": {
- "version": "22.20.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz",
- "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.21.0"
- }
- },
- "node_modules/@types/readable-stream": {
- "version": "4.0.24",
- "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz",
- "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@ztimson/utils": {
"version": "0.29.5",
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz",
@@ -117,18 +658,6 @@
"var-persist": "^1.0.1"
}
},
- "node_modules/abort-controller": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
- "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
- "license": "MIT",
- "dependencies": {
- "event-target-shim": "^5.0.0"
- },
- "engines": {
- "node": ">=6.5"
- }
- },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -151,66 +680,6 @@
"node": ">=12.0"
}
},
- "node_modules/aedes": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/aedes/-/aedes-1.1.2.tgz",
- "integrity": "sha512-prT8Zr4JkqpJMb5rMlLrW54oRyKPNgpRgqrF6PWQedL4ju+1YIwzkraxr16Rm4QyEkpqmZUdOEkfGp7zaDd5Lw==",
- "license": "MIT",
- "dependencies": {
- "aedes-packet": "^4.0.1",
- "aedes-persistence": "^10.3.1",
- "mqemitter": "^7.1.0",
- "mqtt-packet": "^9.0.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/aedes"
- },
- "peerDependencies": {
- "aedes-persistence-level": "^9.1.2",
- "aedes-persistence-mongodb": "^9.3.1",
- "aedes-persistence-redis": "^11.2.1"
- },
- "peerDependenciesMeta": {
- "aedes-persistence-level": {
- "optional": true
- },
- "aedes-persistence-mongodb": {
- "optional": true
- },
- "aedes-persistence-redis": {
- "optional": true
- }
- }
- },
- "node_modules/aedes-packet": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-4.0.1.tgz",
- "integrity": "sha512-PLjKJElXEYB66RzHhJCw0SrRLfSmGRbbg2mtO5t/6FAbFp8vC0cG5ErhvPvfa9iKBrKZwoluWLgn8doBU8sRAw==",
- "license": "MIT",
- "dependencies": {
- "mqtt-packet": "^9.0.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/aedes-persistence": {
- "version": "10.3.1",
- "resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-10.3.1.tgz",
- "integrity": "sha512-C/z0zLEXY9PkUH+7pz31H5gGzlQZxqhFONK1lt5VON0hiHXQq6tr4cSIAtFUVQ1kyUdd7SsfkeKLAECp3gjjpw==",
- "license": "MIT",
- "dependencies": {
- "aedes-packet": "^4.0.1",
- "qlobber": "^8.0.1"
- },
- "engines": {
- "node": ">=20"
- }
- },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -723,24 +1192,6 @@
"node": ">= 0.6"
}
},
- "node_modules/event-target-shim": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
- "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/events": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
- "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8.x"
- }
- },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -796,16 +1247,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/fastparallel": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz",
- "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==",
- "license": "ISC",
- "dependencies": {
- "reusify": "^1.0.4",
- "xtend": "^4.0.2"
- }
- },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -1142,105 +1583,6 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
- "node_modules/mqemitter": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-7.1.0.tgz",
- "integrity": "sha512-GnBDNz3lxmllW201ne0mrmdy5tPOTnc79jjVcsfUa2LG2pUGeyGWVeiae6ZysfC/64XrYOqCKRAQYrB7pGyBVQ==",
- "license": "ISC",
- "dependencies": {
- "fastparallel": "^2.4.1",
- "qlobber": "^8.0.1"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/mqtt-packet": {
- "version": "9.0.2",
- "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
- "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
- "license": "MIT",
- "dependencies": {
- "bl": "^6.0.8",
- "debug": "^4.3.4",
- "process-nextick-args": "^2.0.1"
- }
- },
- "node_modules/mqtt-packet/node_modules/bl": {
- "version": "6.1.6",
- "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
- "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
- "license": "MIT",
- "dependencies": {
- "@types/readable-stream": "^4.0.0",
- "buffer": "^6.0.3",
- "inherits": "^2.0.4",
- "readable-stream": "^4.2.0"
- }
- },
- "node_modules/mqtt-packet/node_modules/buffer": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.2.1"
- }
- },
- "node_modules/mqtt-packet/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/mqtt-packet/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/mqtt-packet/node_modules/readable-stream": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
- "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
- "license": "MIT",
- "dependencies": {
- "abort-controller": "^3.0.0",
- "buffer": "^6.0.3",
- "events": "^3.3.0",
- "process": "^0.11.10",
- "string_decoder": "^1.3.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- }
- },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1428,21 +1770,6 @@
"node": ">=10"
}
},
- "node_modules/process": {
- "version": "0.11.10",
- "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
- "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
- "node_modules/process-nextick-args": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "license": "MIT"
- },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1466,15 +1793,6 @@
"once": "^1.3.1"
}
},
- "node_modules/qlobber": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz",
- "integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==",
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
"node_modules/qs": {
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
@@ -1544,16 +1862,6 @@
"node": ">= 6"
}
},
- "node_modules/reusify": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
- "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
- "license": "MIT",
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1643,6 +1951,55 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
+ "node_modules/sharp": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
+ "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@img/colour": "^1.1.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.8.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.35.4",
+ "@img/sharp-darwin-x64": "0.35.4",
+ "@img/sharp-freebsd-wasm32": "0.35.4",
+ "@img/sharp-libvips-darwin-arm64": "1.3.3",
+ "@img/sharp-libvips-darwin-x64": "1.3.3",
+ "@img/sharp-libvips-linux-arm": "1.3.3",
+ "@img/sharp-libvips-linux-arm64": "1.3.3",
+ "@img/sharp-libvips-linux-ppc64": "1.3.3",
+ "@img/sharp-libvips-linux-riscv64": "1.3.3",
+ "@img/sharp-libvips-linux-s390x": "1.3.3",
+ "@img/sharp-libvips-linux-x64": "1.3.3",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.3",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.3",
+ "@img/sharp-linux-arm": "0.35.4",
+ "@img/sharp-linux-arm64": "0.35.4",
+ "@img/sharp-linux-ppc64": "0.35.4",
+ "@img/sharp-linux-riscv64": "0.35.4",
+ "@img/sharp-linux-s390x": "0.35.4",
+ "@img/sharp-linux-x64": "0.35.4",
+ "@img/sharp-linuxmusl-arm64": "0.35.4",
+ "@img/sharp-linuxmusl-x64": "0.35.4",
+ "@img/sharp-webcontainers-wasm32": "0.35.4",
+ "@img/sharp-win32-arm64": "0.35.4",
+ "@img/sharp-win32-ia32": "0.35.4",
+ "@img/sharp-win32-x64": "0.35.4"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/side-channel": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
@@ -1836,6 +2193,13 @@
"node": ">=0.6"
}
},
+ "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",
+ "optional": true
+ },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -1885,12 +2249,6 @@
"node": ">=20.18.1"
}
},
- "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/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1970,15 +2328,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
- "node_modules/xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4"
- }
- },
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
diff --git a/server/package.json b/server/package.json
index 58ad682..7c3d528 100644
--- a/server/package.json
+++ b/server/package.json
@@ -14,6 +14,7 @@
"cheerio": "^1.2.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
+ "sharp": "^0.35.4",
"yaml": "^2.9.0"
}
}
diff --git a/server/src/adsb.mjs b/server/src/adsb.mjs
index 8f5b86f..47bfcff 100644
--- a/server/src/adsb.mjs
+++ b/server/src/adsb.mjs
@@ -4,25 +4,29 @@ import { fileURLToPath } from 'url';
import * as fs from 'node:fs';
import Database from 'better-sqlite3';
import { fromCsv } from '@ztimson/utils';
-import {getIcon} from './adsb-shapes.mjs';
+import { getIcon } from './adsb-shapes.mjs';
import * as cheerio from 'cheerio';
+import sharp from 'sharp';
-const DIR = dirname(fileURLToPath(import.meta.url));
-const DATA = resolve(DIR, '../data');
-const IMAGES_DIR = resolve(DATA, 'aircraft');
-const DB_PATH = resolve(DATA, 'aircraft.db');
-const CSV_CACHE = resolve(DATA, 'aircraft_db.csv');
-const MAX_HISTORY = 500;
-const HISTORY_TTL = 1000 * 60 * 60; // 1 hour
+const DIR = dirname(fileURLToPath(import.meta.url));
+const DATA = resolve(DIR, '../data');
+const IMAGES_DIR = resolve(DATA, 'aircraft');
+const DB_PATH = resolve(DATA, 'aircraft.db');
+const CSV_CACHE = resolve(DATA, 'aircraft_db.csv');
+const AIRLINES_CACHE = resolve(DATA, 'airlines.dat');
+const MAX_HISTORY = 500;
+const HISTORY_TTL = 1000 * 60 * 60; // 1 hour
const ADSB_TTL = 1000;
-const CSV_URL = 'https://s3.opensky-network.org/data-samples/metadata/aircraft-database-complete-2024-06.csv';
-const MIL_RANGES_URL = ':8080/db-3.14.1708/ranges.js';
+const CSV_URL = 'https://s3.opensky-network.org/data-samples/metadata/aircraft-database-complete-2024-06.csv';
+const AIRLINES_URL = 'https://raw.githubusercontent.com/jpatokal/openflights/master/data/airlines.dat'
-const MILITARY_OPERATORS = ['air force', 'army', 'navy', 'marine', 'coast guard', 'military', 'defence', 'defense', 'luftwaffe', 'RAF', 'USAF', 'USN', 'USMC'];
-const CARGO_OPERATORS = ['fedex', 'ups', 'dhl', 'cargo', 'freight', 'logistic', 'atlas air', 'kalitta', 'air freight'];
-const PASSENGER_OPERATORS = ['airlines', 'airways', 'air ', ' air', 'jetblue', 'easyjet', 'ryanair', 'delta', 'united', 'american', 'avianca', 'southwest', 'lufthansa', 'emirates', 'british'];
-const MILITARY_CLASSES = ['H1M', 'H2M', 'L1M', 'L2M', 'L4M', 'A0'];
+const MILITARY_OPERATORS = ['air force', 'airforce', 'army', 'navy', 'marine', 'coast guard', 'military', 'defence', 'defense', 'law', 'luftwaffe', 'RAF', 'USAF', 'USN', 'USMC'];
+const CARGO_OPERATORS = ['fedex', 'ups', 'dhl', 'cargo', 'freight', 'logistic', 'atlas air', 'kalitta', 'air freight'];
+const PASSENGER_OPERATOR_FALLBACKS = ['airlines', 'airways', 'jetblue', 'easyjet', 'ryanair', 'delta', 'united', 'american', 'avianca', 'southwest', 'lufthansa', 'emirates', 'british'];
+const MILITARY_CLASSES = ['H1M', 'H2M', 'L1M', 'L2M', 'L4M', 'A0'];
+
+let passengerOperators = PASSENGER_OPERATOR_FALLBACKS;
let adsbCache = null;
let adsbCacheTs = 0;
@@ -31,10 +35,51 @@ const history = new Map();
const milRanges = [];
let db;
+function normalizeAirlineName(value) {
+ return String(value || '')
+ .normalize('NFKD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .toLowerCase()
+ .replace(/&/g, ' and ')
+ .replace(/[^a-z0-9]+/g, ' ')
+ .trim()
+ .replace(/\s+/g, ' ');
+}
+
+async function syncAirlines() {
+ try {
+ if (!fs.existsSync(AIRLINES_CACHE)) {
+ console.log('✈️ Downloading airline database');
+ const res = await fetchWithTimeout(AIRLINES_URL, 10000);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ fs.writeFileSync(AIRLINES_CACHE, await res.text());
+ }
+
+ const text = fs.readFileSync(AIRLINES_CACHE, 'utf8');
+ const airlines = fromCsv(text, true);
+ const names = new Set();
+ for (const row of airlines) {
+ if (row.active !== 'Y') continue;
+ for (const value of [row.name, row.alias, row.callsign]) {
+ const normalized = normalizeAirlineName(value);
+ if (normalized.length >= 3) names.add(normalized);
+ }
+ }
+ for (const value of PASSENGER_OPERATOR_FALLBACKS) names.add(normalizeAirlineName(value));
+ passengerOperators = [...names].sort((a, b) => b.length - a.length);
+ console.log(`✈️ Airline operators loaded: ${passengerOperators.length}`);
+ } catch (e) {
+ console.warn(`⚠️ Could not load airline database: ${e.message}`);
+ passengerOperators = PASSENGER_OPERATOR_FALLBACKS.map(normalizeAirlineName);
+ }
+}
+
setInterval(() => {
const cutoff = Date.now() - HISTORY_TTL;
+
for (const [key, trail] of history) {
const last = trail.at(-1);
+
if (!last || last.ts < cutoff) history.delete(key);
}
}, 1000 * 60 * 5);
@@ -48,16 +93,16 @@ function fetchWithTimeout(url, ms = 5000) {
async function syncMilitaryRanges() {
const { ADSB_URL } = cfg();
try {
- const res = await fetch(ADSB_URL + MIL_RANGES_URL);
+ const page = await fetch(ADSB_URL + ':8080').then(resp => resp.text());
+ const db = /databaseFolder = "(db-.+?)"/.exec(page);
+ const res = await fetch(ADSB_URL + `:8080/${db[1]}/ranges.js`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { military } = await res.json();
-
db.exec('DELETE FROM military_ranges');
const insert = db.prepare('INSERT INTO military_ranges (start, end) VALUES (?, ?)');
const insertMany = db.transaction(ranges => {
for (const [s, e] of ranges) insert.run(s.toUpperCase(), e.toUpperCase());
});
-
insertMany(military);
milRanges.length = 0;
milRanges.push(...military.map(([s, e]) => [s.toUpperCase(), e.toUpperCase()]));
@@ -70,72 +115,81 @@ async function syncMilitaryRanges() {
export async function initAircraftDb() {
if (!fs.existsSync(DATA)) fs.mkdirSync(DATA, { recursive: true });
-
+ if (!fs.existsSync(IMAGES_DIR)) fs.mkdirSync(IMAGES_DIR, { recursive: true });
const missing = !fs.existsSync(DB_PATH);
if (missing) console.log(`✈️ Building database`);
-
db = new Database(DB_PATH);
db.exec(`
CREATE TABLE IF NOT EXISTS aircraft (
- icao TEXT PRIMARY KEY,
- built TEXT,
- categoryDescription TEXT,
- country TEXT,
- engines TEXT,
- class TEXT,
- manufacturer TEXT,
- model TEXT,
- modes TEXT,
- operator TEXT,
- operatorCallsign TEXT,
- owner TEXT,
- registration TEXT,
- serialNumber TEXT,
- aircraft TEXT
+ icao TEXT PRIMARY KEY,
+ built TEXT,
+ categoryDescription TEXT,
+ country TEXT,
+ engines TEXT,
+ class TEXT,
+ manufacturer TEXT,
+ model TEXT,
+ modes TEXT,
+ operator TEXT,
+ operatorCallsign TEXT,
+ owner TEXT,
+ registration TEXT,
+ serialNumber TEXT,
+ aircraft TEXT
);
- CREATE TABLE IF NOT EXISTS military_ranges (
- start TEXT NOT NULL,
- end TEXT NOT NULL
- );
- `);
+ CREATE TABLE IF NOT EXISTS military_ranges (start TEXT NOT NULL, end TEXT NOT NULL);
+ `);
+
+ await syncAirlines();
await syncMilitaryRanges();
- if (missing) {
+ if(missing) {
const res = await fetch(CSV_URL);
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ if(!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
fs.writeFileSync(CSV_CACHE, text);
-
const csv = fromCsv(text, true);
const insert = db.prepare(`
INSERT OR REPLACE INTO aircraft VALUES (
- @icao, @built, @categoryDescription, @country, @engines,
- @class, @manufacturer, @model, @modes, @operator,
- @operatorCallsign, @owner, @registration, @serialNumber, @aircraft
+ @icao,
+ @built,
+ @categoryDescription,
+ @country,
+ @engines,
+ @class,
+ @manufacturer,
+ @model,
+ @modes,
+ @operator,
+ @operatorCallsign,
+ @owner,
+ @registration,
+ @serialNumber,
+ @aircraft
)
- `);
+ `);
const insertMany = db.transaction(rows => {
for (const row of rows) insert.run(row);
});
insertMany(csv.map(row => ({
- icao: (row.icao24 || '').toUpperCase(),
- built: row.built || null,
+ icao: (row.icao24 || '').toUpperCase(),
+ built: row.built || null,
categoryDescription: row.categoryDescription || null,
- country: row.country || null,
- engines: row.engines || null,
- class: row.icaoAircraftClass || null,
- manufacturer: row.manufacturerName || row.manufacturerIcao || null,
- model: row.model || null,
- modes: row.modes || null,
- operator: row.operator || null,
- operatorCallsign: row.operatorCallsign || null,
- owner: row.owner || null,
- registration: row.registration || null,
- serialNumber: row.serialNumber || null,
- aircraft: row.typecode || null,
+ country: row.country || null,
+ engines: row.engines || null,
+ class: row.icaoAircraftClass || null,
+ manufacturer: row.manufacturerName || row.manufacturerIcao || null,
+ model: row.model || null,
+ modes: row.modes || null,
+ operator: row.operator || null,
+ operatorCallsign: row.operatorCallsign || null,
+ owner: row.owner || null,
+ registration: row.registration || null,
+ serialNumber: row.serialNumber || null,
+ aircraft: row.typecode || null
})));
fs.unlinkSync(CSV_CACHE);
@@ -161,17 +215,26 @@ function wordMatch(text, keyword) {
export function classifyAircraft(row) {
if (!row) return 'unknown';
- const matchesAny = (fields, list) => fields.some(f => list.some(k => f.toLowerCase().includes(k)));
+ const matchesAny = (fields, list) => fields.some(f => {
+ const value = normalizeAirlineName(f);
+ return value && list.some(k => value.includes(normalizeAirlineName(k)));
+ });
const operatorFields = [row.operator, row.operatorCallsign];
- const ownerFields = [row.owner];
- const allFields = [...operatorFields, ...ownerFields, row.categoryDescription];
+ const ownerFields = [row.owner];
+ const allFields = [...operatorFields, ...ownerFields, row.categoryDescription];
- if (isIcaoInMilitaryRange(row.icao) || MILITARY_CLASSES.includes(row.class) || matchesAny(allFields, MILITARY_OPERATORS)) return 'military';
- if (row.categoryDescription?.toLowerCase().includes('cargo') || matchesAny(operatorFields, CARGO_OPERATORS)) return 'cargo';
- if (matchesAny(operatorFields, PASSENGER_OPERATORS)) return 'passenger';
+ if (isIcaoInMilitaryRange(row.icao) || MILITARY_CLASSES.includes(row.class) ||
+ matchesAny(allFields, MILITARY_OPERATORS)) {
+ return 'military';
+ }
+
+ if (row.categoryDescription?.toLowerCase().includes('cargo') || matchesAny(operatorFields, CARGO_OPERATORS)) {
+ return 'cargo';
+ }
+
+ if (matchesAny(operatorFields, passengerOperators)) return 'passenger';
if (row.owner && !row.operator) return 'private';
-
return 'unknown';
}
@@ -182,9 +245,9 @@ async function fetchHexDb(icao) {
return {
registration: found.Registration || null,
manufacturer: found.Manufacturer || null,
- aircraft: found.ICAOTypeCode || null,
- model: found.Type || null,
- operator: found.RegisteredOwners || null,
+ aircraft: found.ICAOTypeCode || null,
+ model: found.Type || null,
+ operator: found.RegisteredOwners || null
};
}
@@ -192,118 +255,123 @@ async function scrapeHexDatabase(icao) {
const url = `https://hexdatabase.com/h/${icao}`;
const resp = await fetch(url);
if (!resp.ok) return null;
-
const html = await resp.text();
const tableMatch = html.match(/
/i);
if (!tableMatch) return null;
-
const $ = cheerio.load(tableMatch[0]);
const row = $('tr').filter((_, el) => $(el).text().toLowerCase().includes(icao.toLowerCase())).first();
if (!row.length) return null;
-
const cells = row.find('td');
return {
registration: $(cells[1]).text().trim() || null,
- aircraft: $(cells[2]).text().trim() || null,
- operator: $(cells[3]).text().trim() || null,
- serialNumber: $(cells[5]).text().trim() || null,
+ aircraft: $(cells[2]).text().trim() || null,
+ operator: $(cells[3]).text().trim() || null,
+ serialNumber: $(cells[5]).text().trim() || null
};
}
function backfillFromSimilar(aircraft) {
if (!aircraft) return {};
const similar = db.prepare(`
- SELECT manufacturer, model, engines, categoryDescription, class
+ SELECT
+ manufacturer,
+ model,
+ engines,
+ categoryDescription,
+ class
FROM aircraft
WHERE aircraft = ?
- AND (manufacturer IS NOT NULL OR model IS NOT NULL OR engines IS NOT NULL)
- LIMIT 1
- `).get(aircraft);
+ AND (
+ manufacturer IS NOT NULL
+ OR model IS NOT NULL
+ OR engines IS NOT NULL
+ )
+ LIMIT 1
+ `).get(aircraft);
if (!similar) return {};
return {
- manufacturer: similar.manufacturer || null,
- model: similar.model || null,
- engines: similar.engines || null,
+ manufacturer: similar.manufacturer || null,
+ model: similar.model || null,
+ engines: similar.engines || null,
categoryDescription: similar.categoryDescription || null,
- class: similar.class || null,
+ class: similar.class || null
};
}
export async function enrichAircraft(a) {
if (!a.hex) return a;
const icao = a.hex.toUpperCase();
-
- // 1. Check own DB first
const row = db.prepare('SELECT * FROM aircraft WHERE icao = ?').get(icao);
- if(row?.aircraft) return { ...a, ...row, type: classifyAircraft(row) };
-
- // 2. Race the two external sources
- if(noRecord.includes(icao)) return { icao, ...a, type: 'unknown' };
+ if (row?.aircraft) return { ...a, ...row, type: classifyAircraft(row) };
+ if (noRecord.includes(icao)) return { icao, ...a, type: 'unknown' };
const hexDbPromise = fetchHexDb(icao);
const scrapePromise = scrapeHexDatabase(icao);
let found = await hexDbPromise.catch(() => {});
- if(!found) found = await scrapePromise.catch(() => {});
- if(!found) {
+ if (!found) found = await scrapePromise.catch(() => {});
+ if (!found) {
noRecord.push(icao);
return { icao, ...a, type: 'unknown' };
}
-
- // 3. Backfill manufacturer/model/engines from similar aircraft type in DB
const merged = { ...row, ...found };
if (merged.aircraft && (!merged.manufacturer || !merged.model)) {
const similar = backfillFromSimilar(merged.aircraft);
Object.assign(merged, similar);
}
-
- // 4. Save back to DB
- if (found) {
+ if(found) {
db.prepare(`
INSERT INTO aircraft (icao, registration, manufacturer, aircraft, model, operator, country, serialNumber, engines, categoryDescription, class)
VALUES (@icao, @registration, @manufacturer, @aircraft, @model, @operator, @country, @serialNumber, @engines, @categoryDescription, @class)
- ON CONFLICT(icao) DO UPDATE SET
- registration = COALESCE(excluded.registration, registration),
- manufacturer = COALESCE(excluded.manufacturer, manufacturer),
- aircraft = COALESCE(excluded.aircraft, aircraft),
- model = COALESCE(excluded.model, model),
- operator = COALESCE(excluded.operator, operator),
- country = COALESCE(excluded.country, country),
- serialNumber = COALESCE(excluded.serialNumber, serialNumber),
- engines = COALESCE(excluded.engines, engines),
- categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription),
- class = COALESCE(excluded.class, class)
- `).run({
+ ON CONFLICT(icao) DO UPDATE SET
+ registration = COALESCE(excluded.registration, registration),
+ manufacturer = COALESCE(excluded.manufacturer, manufacturer),
+ aircraft = COALESCE(excluded.aircraft, aircraft),
+ model = COALESCE(excluded.model, model),
+ operator = COALESCE(excluded.operator, operator),
+ country = COALESCE(excluded.country, country),
+ serialNumber = COALESCE(excluded.serialNumber, serialNumber),
+ engines = COALESCE(excluded.engines, engines),
+ categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription),
+ class = COALESCE(excluded.class, class)
+ `).run({
icao,
- registration: merged.registration || null,
- manufacturer: merged.manufacturer || null,
- aircraft: merged.aircraft || null,
- model: merged.model || null,
- operator: merged.operator || null,
- country: merged.country || null,
- serialNumber: merged.serialNumber || null,
- engines: merged.engines || null,
+ registration: merged.registration || null,
+ manufacturer: merged.manufacturer || null,
+ aircraft: merged.aircraft || null,
+ model: merged.model || null,
+ operator: merged.operator || null,
+ country: merged.country || null,
+ serialNumber: merged.serialNumber || null,
+ engines: merged.engines || null,
categoryDescription: merged.categoryDescription || null,
- class: merged.class || null,
+ class: merged.class || null
});
}
- return {icao, ...a, ...merged, type: classifyAircraft(merged) }
+ return { icao, ...a, ...merged, type: classifyAircraft(merged) };
}
export async function getADSB() {
- if (adsbCache && Date.now() - adsbCacheTs < ADSB_TTL) return adsbCache;
+ if(adsbCache && Date.now() - adsbCacheTs < ADSB_TTL) return adsbCache;
+
const { ADSB_URL } = cfg();
- if (!ADSB_URL) return [];
+ if(!ADSB_URL) return [];
+
const r = await fetchWithTimeout(`${ADSB_URL}:8080/data/aircraft.json`);
const j = await r.json();
const aircraft = j.aircraft || [];
for (const a of aircraft) {
a.type = 'unknown';
- if (!a.hex || !a.lat || !a.lon) continue;
+ if(!a.hex || !a.lat || !a.lon) continue;
const key = a.hex.toLowerCase();
if (!history.has(key)) history.set(key, []);
const trail = history.get(key);
- trail.push({ latitude: a.lat, longitude: a.lon, altitude: a.alt_baro || 0, ts: Date.now() });
+ trail.push({
+ latitude: a.lat,
+ longitude: a.lon,
+ altitude: a.alt_baro || 0,
+ ts: Date.now()
+ });
if (trail.length > MAX_HISTORY) trail.shift();
}
@@ -334,7 +402,7 @@ export async function getADSBImage(icao) {
const builds = [
`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`,
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`,
- `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`,
+ `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`
];
return builds[~~(Math.random() * builds.length)];
}
@@ -344,6 +412,8 @@ export async function getADSBImage(icao) {
const res = await fetch(`https://duckduckgo.com/?q=${encodeURIComponent(query)}`, {
headers: { 'User-Agent': randomUA() }
});
+
+ if(!res.ok) console.warn(res.status, res.statusText);
const html = await res.text();
const match = html.match(/vqd=['"]?([\d-]+)['"]?/);
return match?.[1];
@@ -354,8 +424,9 @@ export async function getADSBImage(icao) {
const url = `https://duckduckgo.com/i.js?q=${encodeURIComponent(query)}&o=json&vqd=${vqd}&f=,,,,,&p=1`;
const data = await fetch(url, {
- headers: { 'User-Agent': randomUA(), 'Referer': 'https://duckduckgo.com/' }
+ headers: {'User-Agent': randomUA(), 'Referer': 'https://duckduckgo.com/'}
}).then(resp => resp.json());
+ if(!data.ok) console.warn(data.status, data.statusText);
if (data?.results?.length) {
for (const result of data.results) {
@@ -363,9 +434,10 @@ export async function getADSBImage(icao) {
try {
const imgRes = await fetch(result.image);
if (imgRes.ok) return imgRes.blob();
- } catch { }
+ } catch {}
}
}
+
return null;
}
@@ -373,30 +445,41 @@ export async function getADSBImage(icao) {
if (!modelType) return null;
const filePath = resolve(IMAGES_DIR, 'generic', `${modelType}.jpg`);
if (fs.existsSync(filePath)) return fs.readFileSync(filePath);
-
const blob = await duckduckgo(`${modelType} Aircraft`);
if (!blob) return null;
-
fs.mkdirSync(dirname(filePath), { recursive: true });
const buffer = Buffer.from(await blob.arrayBuffer());
- fs.writeFileSync(filePath, buffer);
- return buffer;
+ const metadata = await sharp(buffer).metadata();
+ const width = metadata.width || 1200;
+ const height = metadata.height || 800;
+ const fontSize = Math.max(48, Math.round(Math.min(width, height) * 0.16));
+ const watermark = Buffer.from(`
+`);
+ const watermarked = await sharp(buffer).composite([{ input: watermark }]).jpeg().toBuffer();
+ fs.writeFileSync(filePath, watermarked);
+ return watermarked;
}
async function icaoImage(icao) {
const filePath = resolve(IMAGES_DIR, `${icao}.jpg`);
- if (fs.existsSync(filePath)) return fs.readFileSync(filePath);
-
+ if(fs.existsSync(filePath)) return fs.readFileSync(filePath);
const planeSpotters = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao}`, {
headers: { 'User-Agent': 'open-sight.net (zaktimson@gmail.com)' }
}).then(resp => resp.ok ? resp.json() : null).catch(() => null);
const src = planeSpotters?.photos?.[0]?.thumbnail_large?.src || planeSpotters?.photos?.[0]?.thumbnail?.src;
- if (!src) return null;
-
- const resp = await fetch(src, { headers: { 'Accept': '*/*', 'User-Agent': randomUA() } });
- if (!resp.ok) return null;
+ if(!src) return null;
+ const resp = await fetch(src, {
+ headers: {'Accept': '*/*', 'User-Agent': randomUA()}
+ });
+ if(!resp.ok) return null;
fs.mkdirSync(IMAGES_DIR, { recursive: true });
const buffer = Buffer.from(await resp.arrayBuffer());
fs.writeFileSync(filePath, buffer);
@@ -404,9 +487,9 @@ export async function getADSBImage(icao) {
}
icao = icao.toLowerCase();
+ if(!fs.existsSync(IMAGES_DIR)) fs.mkdirSync(IMAGES_DIR, { recursive: true });
const aircraft = await enrichAircraft({ hex: icao });
const modelType = aircraft.aircraft || aircraft.model || aircraft.type;
-
const generic = genericImage(modelType);
const specific = await icaoImage(icao);
return specific || await generic;
diff --git a/server/src/sonde.mjs b/server/src/sonde.mjs
index f45fa33..9532f96 100644
--- a/server/src/sonde.mjs
+++ b/server/src/sonde.mjs
@@ -33,20 +33,17 @@ export async function getSondes() {
altitude: t.alt,
heading: t.heading,
speed: t.vel_h,
- vertical_speed: t.vel_v,
- sats: t.sats,
- timestamp: t.datetime ? Date.parse(t.datetime) : sonde.timestamp * 1000,
- datetime: t.datetime,
+ climb: t.vel_v,
+ gps: t.sats,
+ timestamp: t.datetime ? Date.parse(t.datetime) : new Date(sonde.timestamp * 1000),
frame: t.frame,
battery: t.batt,
temperature: t.temp,
humidity: t.humidity,
pressure: t.pressure,
- frequency: t.freq_float,
- frequency_hz: t.tx_frequency,
- snr: t.snr,
ppm: t.ppm,
- aprsid: t.aprsid?.trim(),
+ freqMHz: t.tx_frequency / 1000,
+ snr: t.snr,
rs41_mainboard: t.rs41_mainboard,
rs41_mainboard_fw: t.rs41_mainboard_fw,
version: t.version,