better military icao detection, airlines and stock image indicator

This commit is contained in:
2026-09-13 23:33:47 -04:00
parent 746ec877da
commit a255638f73
8 changed files with 863 additions and 431 deletions

View File

@@ -200,7 +200,7 @@ const navball = computed(() => {
z-index: 100; z-index: 100;
pointer-events: auto; pointer-events: auto;
background: rgba(0,0,0,0.88); background: rgba(0,0,0,0.88);
border: 1px solid rgba(200,200,220,0.3); border: 1px solid rgba(0, 255, 0, 0.3);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,0.5); box-shadow: 0 4px 24px rgba(0,0,0,0.5);
overflow: hidden; overflow: hidden;
@@ -213,7 +213,7 @@ const navball = computed(() => {
.ap-header { .ap-header {
position: relative; position: relative;
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid rgba(200,200,220,0.2); border-bottom: 1px solid rgba(0, 255, 0, 0.2);
cursor: move; cursor: move;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;

View File

@@ -390,7 +390,7 @@ function restoreOverlayState() {
if (activeOverlays.value.has('rain')) refreshRain(); if (activeOverlays.value.has('rain')) refreshRain();
} }
function getAutoLayer(category: AutoCategory) { function getAutoLayer(category: AutoCategory): any {
if (category === 'aircraft') return airTraffic; if (category === 'aircraft') return airTraffic;
if (category === 'marine') return aisLayer; if (category === 'marine') return aisLayer;
return sats; return sats;
@@ -402,7 +402,7 @@ function getAutoId(category: AutoCategory, target: any): string {
return String(target.satellite); return String(target.satellite);
} }
function getAutoTarget(category: AutoCategory, id: string) { function getAutoTarget(category: AutoCategory, id: any) {
if (category === 'aircraft') return airTraffic.getById(id); if (category === 'aircraft') return airTraffic.getById(id);
if (category === 'marine') return aisLayer.getById(id); if (category === 'marine') return aisLayer.getById(id);
return sats.getById(id); return sats.getById(id);
@@ -444,9 +444,7 @@ function focusAutoTarget(category: AutoCategory, target: any, animate = true) {
function closeAutoPopup() { function closeAutoPopup() {
if (!autoCategory.value || !autoTarget.value) return; if (!autoCategory.value || !autoTarget.value) return;
getAutoLayer(autoCategory.value).closeAutoPopup( getAutoLayer(autoCategory.value).closeAutoPopup(getAutoId(autoCategory.value, autoTarget.value));
getAutoId(autoCategory.value, autoTarget.value),
);
} }
function clearAutoTarget() { function clearAutoTarget() {
@@ -520,15 +518,11 @@ function getAutoDistance(object: AutoObject): number {
} }
function getNewest(objects: AutoObject[]): AutoObject | null { function getNewest(objects: AutoObject[]): AutoObject | null {
return objects return objects.slice().sort((a, b) => b.discoveredAt - a.discoveredAt)[0] ?? null;
.slice()
.sort((a, b) => b.discoveredAt - a.discoveredAt)[0] ?? null;
} }
function getClosest(objects: AutoObject[]): AutoObject | null { function getClosest(objects: AutoObject[]): AutoObject | null {
return objects return objects.slice().sort((a, b) => getAutoDistance(a) - getAutoDistance(b))[0] ?? null;
.slice()
.sort((a, b) => getAutoDistance(a) - getAutoDistance(b))[0] ?? null;
} }
function setAutoTarget(object: AutoObject) { function setAutoTarget(object: AutoObject) {
@@ -548,10 +542,8 @@ function setAutoTarget(object: AutoObject) {
function shouldNewTargetSteal(currentObject: AutoObject, newObject: AutoObject): boolean { function shouldNewTargetSteal(currentObject: AutoObject, newObject: AutoObject): boolean {
const currentDistance = getAutoDistance(currentObject); const currentDistance = getAutoDistance(currentObject);
const newDistance = getAutoDistance(newObject); const newDistance = getAutoDistance(newObject);
if (!isFinite(currentDistance)) return true; if (!isFinite(currentDistance)) return true;
if (!isFinite(newDistance)) return false; if (!isFinite(newDistance)) return false;
return newDistance <= currentDistance * AUTO_DISTANCE_MARGIN; return newDistance <= currentDistance * AUTO_DISTANCE_MARGIN;
} }

View File

@@ -204,7 +204,7 @@ const compass = computed(() => {
z-index: 100; z-index: 100;
pointer-events: auto; pointer-events: auto;
background: rgba(0,0,0,0.88); background: rgba(0,0,0,0.88);
border: 1px solid rgba(200,200,220,0.3); border: 1px solid rgba(0,255,255,0.3);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,0.5); box-shadow: 0 4px 24px rgba(0,0,0,0.5);
overflow: hidden; overflow: hidden;
@@ -217,7 +217,7 @@ const compass = computed(() => {
.sp-header { .sp-header {
position: relative; position: relative;
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid rgba(200,200,220,0.2); border-bottom: 1px solid rgba(0,255,255,0.2);
cursor: move; cursor: move;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;

View File

@@ -424,7 +424,7 @@ onUnmounted(() => {
<div class="tp-meta"> <div class="tp-meta">
<div class="flex-c flex-fill"> <div class="flex-c flex-fill">
<span>Type: {{ latest?.type || 'Unknown' }}</span> <span>Type: {{ latest?.type || 'Unknown' }}</span>
<span>Freq: {{ latest?.frequency?.toFixed(3) || '—' }} MHz</span> <span>Freq: {{ latest?.frequency?.toFixed(2) || '—' }} MHz</span>
</div> </div>
<div class="flex-c flex-fill align-x-end"> <div class="flex-c flex-fill align-x-end">
@@ -486,7 +486,7 @@ onUnmounted(() => {
<div class="tp-environment"> <div class="tp-environment">
<div class="tp-environment-label">Environment</div> <div class="tp-environment-label">Environment</div>
<div class="tp-environment-cards gap-4"> <div class="tp-environment-cards gap-1">
<div class="tp-environment-card"> <div class="tp-environment-card">
<span class="tp-environment-icon">🌡</span> <span class="tp-environment-icon">🌡</span>
<div class="tp-environment-info"> <div class="tp-environment-info">
@@ -507,6 +507,16 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<div class="tp-environment-card">
<span class="tp-environment-icon"></span>
<div class="tp-environment-info">
<span class="tp-environment-stat-label">Pressure</span>
<span class="tp-environment-stat-value">
{{ latest?.pressure > 0 ? latest.pressure : '—' }} hpa
</span>
</div>
</div>
<div class="tp-environment-card"> <div class="tp-environment-card">
<span class="tp-environment-icon">🫧</span> <span class="tp-environment-icon">🫧</span>
<div class="tp-environment-info"> <div class="tp-environment-info">
@@ -525,7 +535,7 @@ onUnmounted(() => {
:currentAltitude="currentAltitude" :currentAltitude="currentAltitude"
:unit="altitudeUnit.label" :unit="altitudeUnit.label"
start-time=" " start-time=" "
:end-time="formatDate('HH:MM A', latest.timestamp)" :end-time="formatDate('HH:mm:ss', latest.timestamp)"
color="#ffaa00" color="#ffaa00"
/> />

881
server/package-lock.json generated
View File

@@ -15,9 +15,568 @@
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"sharp": "^0.35.4",
"yaml": "^2.9.0" "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": { "node_modules/@scalar/client-side-rendering": {
"version": "0.2.6", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/@scalar/client-side-rendering/-/client-side-rendering-0.2.6.tgz", "resolved": "https://registry.npmjs.org/@scalar/client-side-rendering/-/client-side-rendering-0.2.6.tgz",
@@ -90,24 +649,6 @@
"node": ">=20" "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": { "node_modules/@ztimson/utils": {
"version": "0.29.5", "version": "0.29.5",
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz", "resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz",
@@ -117,18 +658,6 @@
"var-persist": "^1.0.1" "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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -151,66 +680,6 @@
"node": ">=12.0" "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": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -723,24 +1192,6 @@
"node": ">= 0.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": { "node_modules/expand-template": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -796,16 +1247,6 @@
"url": "https://opencollective.com/express" "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": { "node_modules/file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "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==", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT" "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": { "node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1428,21 +1770,6 @@
"node": ">=10" "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": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1466,15 +1793,6 @@
"once": "^1.3.1" "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": { "node_modules/qs": {
"version": "6.15.3", "version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
@@ -1544,16 +1862,6 @@
"node": ">= 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": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1643,6 +1951,55 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC" "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": { "node_modules/side-channel": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
@@ -1836,6 +2193,13 @@
"node": ">=0.6" "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": { "node_modules/tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -1885,12 +2249,6 @@
"node": ">=20.18.1" "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": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1970,15 +2328,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "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": { "node_modules/yaml": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",

View File

@@ -14,6 +14,7 @@
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"sharp": "^0.35.4",
"yaml": "^2.9.0" "yaml": "^2.9.0"
} }
} }

View File

@@ -4,25 +4,29 @@ import { fileURLToPath } from 'url';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import Database from 'better-sqlite3'; import Database from 'better-sqlite3';
import { fromCsv } from '@ztimson/utils'; import { fromCsv } from '@ztimson/utils';
import {getIcon} from './adsb-shapes.mjs'; import { getIcon } from './adsb-shapes.mjs';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import sharp from 'sharp';
const DIR = dirname(fileURLToPath(import.meta.url)); const DIR = dirname(fileURLToPath(import.meta.url));
const DATA = resolve(DIR, '../data'); const DATA = resolve(DIR, '../data');
const IMAGES_DIR = resolve(DATA, 'aircraft'); const IMAGES_DIR = resolve(DATA, 'aircraft');
const DB_PATH = resolve(DATA, 'aircraft.db'); const DB_PATH = resolve(DATA, 'aircraft.db');
const CSV_CACHE = resolve(DATA, 'aircraft_db.csv'); const CSV_CACHE = resolve(DATA, 'aircraft_db.csv');
const MAX_HISTORY = 500; const AIRLINES_CACHE = resolve(DATA, 'airlines.dat');
const HISTORY_TTL = 1000 * 60 * 60; // 1 hour const MAX_HISTORY = 500;
const HISTORY_TTL = 1000 * 60 * 60; // 1 hour
const ADSB_TTL = 1000; const ADSB_TTL = 1000;
const CSV_URL = 'https://s3.opensky-network.org/data-samples/metadata/aircraft-database-complete-2024-06.csv'; 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 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 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 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 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']; const MILITARY_CLASSES = ['H1M', 'H2M', 'L1M', 'L2M', 'L4M', 'A0'];
let passengerOperators = PASSENGER_OPERATOR_FALLBACKS;
let adsbCache = null; let adsbCache = null;
let adsbCacheTs = 0; let adsbCacheTs = 0;
@@ -31,10 +35,51 @@ const history = new Map();
const milRanges = []; const milRanges = [];
let db; 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(() => { setInterval(() => {
const cutoff = Date.now() - HISTORY_TTL; const cutoff = Date.now() - HISTORY_TTL;
for (const [key, trail] of history) { for (const [key, trail] of history) {
const last = trail.at(-1); const last = trail.at(-1);
if (!last || last.ts < cutoff) history.delete(key); if (!last || last.ts < cutoff) history.delete(key);
} }
}, 1000 * 60 * 5); }, 1000 * 60 * 5);
@@ -48,16 +93,16 @@ function fetchWithTimeout(url, ms = 5000) {
async function syncMilitaryRanges() { async function syncMilitaryRanges() {
const { ADSB_URL } = cfg(); const { ADSB_URL } = cfg();
try { 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}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { military } = await res.json(); const { military } = await res.json();
db.exec('DELETE FROM military_ranges'); db.exec('DELETE FROM military_ranges');
const insert = db.prepare('INSERT INTO military_ranges (start, end) VALUES (?, ?)'); const insert = db.prepare('INSERT INTO military_ranges (start, end) VALUES (?, ?)');
const insertMany = db.transaction(ranges => { const insertMany = db.transaction(ranges => {
for (const [s, e] of ranges) insert.run(s.toUpperCase(), e.toUpperCase()); for (const [s, e] of ranges) insert.run(s.toUpperCase(), e.toUpperCase());
}); });
insertMany(military); insertMany(military);
milRanges.length = 0; milRanges.length = 0;
milRanges.push(...military.map(([s, e]) => [s.toUpperCase(), e.toUpperCase()])); milRanges.push(...military.map(([s, e]) => [s.toUpperCase(), e.toUpperCase()]));
@@ -70,72 +115,81 @@ async function syncMilitaryRanges() {
export async function initAircraftDb() { export async function initAircraftDb() {
if (!fs.existsSync(DATA)) fs.mkdirSync(DATA, { recursive: true }); 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); const missing = !fs.existsSync(DB_PATH);
if (missing) console.log(`✈️ Building database`); if (missing) console.log(`✈️ Building database`);
db = new Database(DB_PATH); db = new Database(DB_PATH);
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS aircraft ( CREATE TABLE IF NOT EXISTS aircraft (
icao TEXT PRIMARY KEY, icao TEXT PRIMARY KEY,
built TEXT, built TEXT,
categoryDescription TEXT, categoryDescription TEXT,
country TEXT, country TEXT,
engines TEXT, engines TEXT,
class TEXT, class TEXT,
manufacturer TEXT, manufacturer TEXT,
model TEXT, model TEXT,
modes TEXT, modes TEXT,
operator TEXT, operator TEXT,
operatorCallsign TEXT, operatorCallsign TEXT,
owner TEXT, owner TEXT,
registration TEXT, registration TEXT,
serialNumber TEXT, serialNumber TEXT,
aircraft 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(); await syncMilitaryRanges();
if (missing) { if(missing) {
const res = await fetch(CSV_URL); 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(); const text = await res.text();
fs.writeFileSync(CSV_CACHE, text); fs.writeFileSync(CSV_CACHE, text);
const csv = fromCsv(text, true); const csv = fromCsv(text, true);
const insert = db.prepare(` const insert = db.prepare(`
INSERT OR REPLACE INTO aircraft VALUES ( INSERT OR REPLACE INTO aircraft VALUES (
@icao, @built, @categoryDescription, @country, @engines, @icao,
@class, @manufacturer, @model, @modes, @operator, @built,
@operatorCallsign, @owner, @registration, @serialNumber, @aircraft @categoryDescription,
@country,
@engines,
@class,
@manufacturer,
@model,
@modes,
@operator,
@operatorCallsign,
@owner,
@registration,
@serialNumber,
@aircraft
) )
`); `);
const insertMany = db.transaction(rows => { const insertMany = db.transaction(rows => {
for (const row of rows) insert.run(row); for (const row of rows) insert.run(row);
}); });
insertMany(csv.map(row => ({ insertMany(csv.map(row => ({
icao: (row.icao24 || '').toUpperCase(), icao: (row.icao24 || '').toUpperCase(),
built: row.built || null, built: row.built || null,
categoryDescription: row.categoryDescription || null, categoryDescription: row.categoryDescription || null,
country: row.country || null, country: row.country || null,
engines: row.engines || null, engines: row.engines || null,
class: row.icaoAircraftClass || null, class: row.icaoAircraftClass || null,
manufacturer: row.manufacturerName || row.manufacturerIcao || null, manufacturer: row.manufacturerName || row.manufacturerIcao || null,
model: row.model || null, model: row.model || null,
modes: row.modes || null, modes: row.modes || null,
operator: row.operator || null, operator: row.operator || null,
operatorCallsign: row.operatorCallsign || null, operatorCallsign: row.operatorCallsign || null,
owner: row.owner || null, owner: row.owner || null,
registration: row.registration || null, registration: row.registration || null,
serialNumber: row.serialNumber || null, serialNumber: row.serialNumber || null,
aircraft: row.typecode || null, aircraft: row.typecode || null
}))); })));
fs.unlinkSync(CSV_CACHE); fs.unlinkSync(CSV_CACHE);
@@ -161,17 +215,26 @@ function wordMatch(text, keyword) {
export function classifyAircraft(row) { export function classifyAircraft(row) {
if (!row) return 'unknown'; 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 operatorFields = [row.operator, row.operatorCallsign];
const ownerFields = [row.owner]; const ownerFields = [row.owner];
const allFields = [...operatorFields, ...ownerFields, row.categoryDescription]; const allFields = [...operatorFields, ...ownerFields, row.categoryDescription];
if (isIcaoInMilitaryRange(row.icao) || MILITARY_CLASSES.includes(row.class) || matchesAny(allFields, MILITARY_OPERATORS)) return 'military'; if (isIcaoInMilitaryRange(row.icao) || MILITARY_CLASSES.includes(row.class) ||
if (row.categoryDescription?.toLowerCase().includes('cargo') || matchesAny(operatorFields, CARGO_OPERATORS)) return 'cargo'; matchesAny(allFields, MILITARY_OPERATORS)) {
if (matchesAny(operatorFields, PASSENGER_OPERATORS)) return 'passenger'; 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'; if (row.owner && !row.operator) return 'private';
return 'unknown'; return 'unknown';
} }
@@ -182,9 +245,9 @@ async function fetchHexDb(icao) {
return { return {
registration: found.Registration || null, registration: found.Registration || null,
manufacturer: found.Manufacturer || null, manufacturer: found.Manufacturer || null,
aircraft: found.ICAOTypeCode || null, aircraft: found.ICAOTypeCode || null,
model: found.Type || null, model: found.Type || null,
operator: found.RegisteredOwners || null, operator: found.RegisteredOwners || null
}; };
} }
@@ -192,118 +255,123 @@ async function scrapeHexDatabase(icao) {
const url = `https://hexdatabase.com/h/${icao}`; const url = `https://hexdatabase.com/h/${icao}`;
const resp = await fetch(url); const resp = await fetch(url);
if (!resp.ok) return null; if (!resp.ok) return null;
const html = await resp.text(); const html = await resp.text();
const tableMatch = html.match(/<table[\s\S]*?<\/table>/i); const tableMatch = html.match(/<table[\s\S]*?<\/table>/i);
if (!tableMatch) return null; if (!tableMatch) return null;
const $ = cheerio.load(tableMatch[0]); const $ = cheerio.load(tableMatch[0]);
const row = $('tr').filter((_, el) => $(el).text().toLowerCase().includes(icao.toLowerCase())).first(); const row = $('tr').filter((_, el) => $(el).text().toLowerCase().includes(icao.toLowerCase())).first();
if (!row.length) return null; if (!row.length) return null;
const cells = row.find('td'); const cells = row.find('td');
return { return {
registration: $(cells[1]).text().trim() || null, registration: $(cells[1]).text().trim() || null,
aircraft: $(cells[2]).text().trim() || null, aircraft: $(cells[2]).text().trim() || null,
operator: $(cells[3]).text().trim() || null, operator: $(cells[3]).text().trim() || null,
serialNumber: $(cells[5]).text().trim() || null, serialNumber: $(cells[5]).text().trim() || null
}; };
} }
function backfillFromSimilar(aircraft) { function backfillFromSimilar(aircraft) {
if (!aircraft) return {}; if (!aircraft) return {};
const similar = db.prepare(` const similar = db.prepare(`
SELECT manufacturer, model, engines, categoryDescription, class SELECT
manufacturer,
model,
engines,
categoryDescription,
class
FROM aircraft FROM aircraft
WHERE aircraft = ? WHERE aircraft = ?
AND (manufacturer IS NOT NULL OR model IS NOT NULL OR engines IS NOT NULL) AND (
LIMIT 1 manufacturer IS NOT NULL
`).get(aircraft); OR model IS NOT NULL
OR engines IS NOT NULL
)
LIMIT 1
`).get(aircraft);
if (!similar) return {}; if (!similar) return {};
return { return {
manufacturer: similar.manufacturer || null, manufacturer: similar.manufacturer || null,
model: similar.model || null, model: similar.model || null,
engines: similar.engines || null, engines: similar.engines || null,
categoryDescription: similar.categoryDescription || null, categoryDescription: similar.categoryDescription || null,
class: similar.class || null, class: similar.class || null
}; };
} }
export async function enrichAircraft(a) { export async function enrichAircraft(a) {
if (!a.hex) return a; if (!a.hex) return a;
const icao = a.hex.toUpperCase(); const icao = a.hex.toUpperCase();
// 1. Check own DB first
const row = db.prepare('SELECT * FROM aircraft WHERE icao = ?').get(icao); const row = db.prepare('SELECT * FROM aircraft WHERE icao = ?').get(icao);
if(row?.aircraft) return { ...a, ...row, type: classifyAircraft(row) }; if (row?.aircraft) return { ...a, ...row, type: classifyAircraft(row) };
if (noRecord.includes(icao)) return { icao, ...a, type: 'unknown' };
// 2. Race the two external sources
if(noRecord.includes(icao)) return { icao, ...a, type: 'unknown' };
const hexDbPromise = fetchHexDb(icao); const hexDbPromise = fetchHexDb(icao);
const scrapePromise = scrapeHexDatabase(icao); const scrapePromise = scrapeHexDatabase(icao);
let found = await hexDbPromise.catch(() => {}); let found = await hexDbPromise.catch(() => {});
if(!found) found = await scrapePromise.catch(() => {}); if (!found) found = await scrapePromise.catch(() => {});
if(!found) { if (!found) {
noRecord.push(icao); noRecord.push(icao);
return { icao, ...a, type: 'unknown' }; return { icao, ...a, type: 'unknown' };
} }
// 3. Backfill manufacturer/model/engines from similar aircraft type in DB
const merged = { ...row, ...found }; const merged = { ...row, ...found };
if (merged.aircraft && (!merged.manufacturer || !merged.model)) { if (merged.aircraft && (!merged.manufacturer || !merged.model)) {
const similar = backfillFromSimilar(merged.aircraft); const similar = backfillFromSimilar(merged.aircraft);
Object.assign(merged, similar); Object.assign(merged, similar);
} }
if(found) {
// 4. Save back to DB
if (found) {
db.prepare(` db.prepare(`
INSERT INTO aircraft (icao, registration, manufacturer, aircraft, model, operator, country, serialNumber, engines, categoryDescription, class) 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) VALUES (@icao, @registration, @manufacturer, @aircraft, @model, @operator, @country, @serialNumber, @engines, @categoryDescription, @class)
ON CONFLICT(icao) DO UPDATE SET ON CONFLICT(icao) DO UPDATE SET
registration = COALESCE(excluded.registration, registration), registration = COALESCE(excluded.registration, registration),
manufacturer = COALESCE(excluded.manufacturer, manufacturer), manufacturer = COALESCE(excluded.manufacturer, manufacturer),
aircraft = COALESCE(excluded.aircraft, aircraft), aircraft = COALESCE(excluded.aircraft, aircraft),
model = COALESCE(excluded.model, model), model = COALESCE(excluded.model, model),
operator = COALESCE(excluded.operator, operator), operator = COALESCE(excluded.operator, operator),
country = COALESCE(excluded.country, country), country = COALESCE(excluded.country, country),
serialNumber = COALESCE(excluded.serialNumber, serialNumber), serialNumber = COALESCE(excluded.serialNumber, serialNumber),
engines = COALESCE(excluded.engines, engines), engines = COALESCE(excluded.engines, engines),
categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription), categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription),
class = COALESCE(excluded.class, class) class = COALESCE(excluded.class, class)
`).run({ `).run({
icao, icao,
registration: merged.registration || null, registration: merged.registration || null,
manufacturer: merged.manufacturer || null, manufacturer: merged.manufacturer || null,
aircraft: merged.aircraft || null, aircraft: merged.aircraft || null,
model: merged.model || null, model: merged.model || null,
operator: merged.operator || null, operator: merged.operator || null,
country: merged.country || null, country: merged.country || null,
serialNumber: merged.serialNumber || null, serialNumber: merged.serialNumber || null,
engines: merged.engines || null, engines: merged.engines || null,
categoryDescription: merged.categoryDescription || 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() { 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(); const { ADSB_URL } = cfg();
if (!ADSB_URL) return []; if(!ADSB_URL) return [];
const r = await fetchWithTimeout(`${ADSB_URL}:8080/data/aircraft.json`); const r = await fetchWithTimeout(`${ADSB_URL}:8080/data/aircraft.json`);
const j = await r.json(); const j = await r.json();
const aircraft = j.aircraft || []; const aircraft = j.aircraft || [];
for (const a of aircraft) { for (const a of aircraft) {
a.type = 'unknown'; a.type = 'unknown';
if (!a.hex || !a.lat || !a.lon) continue; if(!a.hex || !a.lat || !a.lon) continue;
const key = a.hex.toLowerCase(); const key = a.hex.toLowerCase();
if (!history.has(key)) history.set(key, []); if (!history.has(key)) history.set(key, []);
const trail = history.get(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(); if (trail.length > MAX_HISTORY) trail.shift();
} }
@@ -334,7 +402,7 @@ export async function getADSBImage(icao) {
const builds = [ 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 (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 (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)]; 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)}`, { const res = await fetch(`https://duckduckgo.com/?q=${encodeURIComponent(query)}`, {
headers: { 'User-Agent': randomUA() } headers: { 'User-Agent': randomUA() }
}); });
if(!res.ok) console.warn(res.status, res.statusText);
const html = await res.text(); const html = await res.text();
const match = html.match(/vqd=['"]?([\d-]+)['"]?/); const match = html.match(/vqd=['"]?([\d-]+)['"]?/);
return match?.[1]; 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 url = `https://duckduckgo.com/i.js?q=${encodeURIComponent(query)}&o=json&vqd=${vqd}&f=,,,,,&p=1`;
const data = await fetch(url, { 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()); }).then(resp => resp.json());
if(!data.ok) console.warn(data.status, data.statusText);
if (data?.results?.length) { if (data?.results?.length) {
for (const result of data.results) { for (const result of data.results) {
@@ -363,9 +434,10 @@ export async function getADSBImage(icao) {
try { try {
const imgRes = await fetch(result.image); const imgRes = await fetch(result.image);
if (imgRes.ok) return imgRes.blob(); if (imgRes.ok) return imgRes.blob();
} catch { } } catch {}
} }
} }
return null; return null;
} }
@@ -373,30 +445,41 @@ export async function getADSBImage(icao) {
if (!modelType) return null; if (!modelType) return null;
const filePath = resolve(IMAGES_DIR, 'generic', `${modelType}.jpg`); const filePath = resolve(IMAGES_DIR, 'generic', `${modelType}.jpg`);
if (fs.existsSync(filePath)) return fs.readFileSync(filePath); if (fs.existsSync(filePath)) return fs.readFileSync(filePath);
const blob = await duckduckgo(`${modelType} Aircraft`); const blob = await duckduckgo(`${modelType} Aircraft`);
if (!blob) return null; if (!blob) return null;
fs.mkdirSync(dirname(filePath), { recursive: true }); fs.mkdirSync(dirname(filePath), { recursive: true });
const buffer = Buffer.from(await blob.arrayBuffer()); const buffer = Buffer.from(await blob.arrayBuffer());
fs.writeFileSync(filePath, buffer); const metadata = await sharp(buffer).metadata();
return buffer; 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(`
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
font-family="Arial, Helvetica, sans-serif" font-size="${fontSize}px"
font-weight="700" fill="white" fill-opacity="0.65"
stroke="black" stroke-opacity="0.35" stroke-width="${Math.max(2, Math.round(fontSize * 0.04))}"
transform="rotate(-20 ${width / 2} ${height / 2})">STOCK IMAGE</text>
</svg>`);
const watermarked = await sharp(buffer).composite([{ input: watermark }]).jpeg().toBuffer();
fs.writeFileSync(filePath, watermarked);
return watermarked;
} }
async function icaoImage(icao) { async function icaoImage(icao) {
const filePath = resolve(IMAGES_DIR, `${icao}.jpg`); 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}`, { const planeSpotters = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao}`, {
headers: { 'User-Agent': 'open-sight.net (zaktimson@gmail.com)' } headers: { 'User-Agent': 'open-sight.net (zaktimson@gmail.com)' }
}).then(resp => resp.ok ? resp.json() : null).catch(() => null); }).then(resp => resp.ok ? resp.json() : null).catch(() => null);
const src = planeSpotters?.photos?.[0]?.thumbnail_large?.src || planeSpotters?.photos?.[0]?.thumbnail?.src; const src = planeSpotters?.photos?.[0]?.thumbnail_large?.src || planeSpotters?.photos?.[0]?.thumbnail?.src;
if (!src) return null; if(!src) return null;
const resp = await fetch(src, {
const resp = await fetch(src, { headers: { 'Accept': '*/*', 'User-Agent': randomUA() } }); headers: {'Accept': '*/*', 'User-Agent': randomUA()}
if (!resp.ok) return null; });
if(!resp.ok) return null;
fs.mkdirSync(IMAGES_DIR, { recursive: true }); fs.mkdirSync(IMAGES_DIR, { recursive: true });
const buffer = Buffer.from(await resp.arrayBuffer()); const buffer = Buffer.from(await resp.arrayBuffer());
fs.writeFileSync(filePath, buffer); fs.writeFileSync(filePath, buffer);
@@ -404,9 +487,9 @@ export async function getADSBImage(icao) {
} }
icao = icao.toLowerCase(); icao = icao.toLowerCase();
if(!fs.existsSync(IMAGES_DIR)) fs.mkdirSync(IMAGES_DIR, { recursive: true });
const aircraft = await enrichAircraft({ hex: icao }); const aircraft = await enrichAircraft({ hex: icao });
const modelType = aircraft.aircraft || aircraft.model || aircraft.type; const modelType = aircraft.aircraft || aircraft.model || aircraft.type;
const generic = genericImage(modelType); const generic = genericImage(modelType);
const specific = await icaoImage(icao); const specific = await icaoImage(icao);
return specific || await generic; return specific || await generic;

View File

@@ -33,20 +33,17 @@ export async function getSondes() {
altitude: t.alt, altitude: t.alt,
heading: t.heading, heading: t.heading,
speed: t.vel_h, speed: t.vel_h,
vertical_speed: t.vel_v, climb: t.vel_v,
sats: t.sats, gps: t.sats,
timestamp: t.datetime ? Date.parse(t.datetime) : sonde.timestamp * 1000, timestamp: t.datetime ? Date.parse(t.datetime) : new Date(sonde.timestamp * 1000),
datetime: t.datetime,
frame: t.frame, frame: t.frame,
battery: t.batt, battery: t.batt,
temperature: t.temp, temperature: t.temp,
humidity: t.humidity, humidity: t.humidity,
pressure: t.pressure, pressure: t.pressure,
frequency: t.freq_float,
frequency_hz: t.tx_frequency,
snr: t.snr,
ppm: t.ppm, ppm: t.ppm,
aprsid: t.aprsid?.trim(), freqMHz: t.tx_frequency / 1000,
snr: t.snr,
rs41_mainboard: t.rs41_mainboard, rs41_mainboard: t.rs41_mainboard,
rs41_mainboard_fw: t.rs41_mainboard_fw, rs41_mainboard_fw: t.rs41_mainboard_fw,
version: t.version, version: t.version,