diff --git a/app/page.tsx b/app/page.tsx index 40cccb6..9a5eb97 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,6 +10,7 @@ import AircraftForm from "../components/AircraftForm"; import LegalDisclaimerModal from "../components/LegalDisclaimerModal"; import LandingPage from "../components/LandingPage"; import { isPointInPolygon, getCGLimitsAtWeight } from "../utils/calculations"; +import { validateFleetData } from "../utils/validation"; export interface CustomStation { id: string; @@ -48,7 +49,15 @@ export default function Home() { useEffect(() => { const loadedFleet = localStorage.getItem("wb_saved_fleet"); if (loadedFleet) { - try { setSavedPlanes(JSON.parse(loadedFleet)); } catch (e) { console.error(e); } + try { + const parsed = JSON.parse(loadedFleet); + const validated = validateFleetData(parsed); + if (validated) { + setSavedPlanes(validated); + } else { + console.error("Loaded fleet data validation failed or was empty."); + } + } catch (e) { console.error(e); } } const savedTheme = localStorage.getItem("wb_theme"); if (savedTheme === "dark") { @@ -100,13 +109,15 @@ export default function Home() { reader.onload = (event) => { try { const json = JSON.parse(event.target?.result as string); - if (Array.isArray(json)) { - setSavedPlanes(json); - localStorage.setItem("wb_saved_fleet", JSON.stringify(json)); + const validated = validateFleetData(json); + + if (validated) { + setSavedPlanes(validated); + localStorage.setItem("wb_saved_fleet", JSON.stringify(validated)); alert("Fleet imported successfully!"); setShowSettings(false); } else { - alert("Invalid file format."); + alert("Invalid file format or corrupted data."); } } catch (err) { alert("Error parsing file."); diff --git a/package-lock.json b/package-lock.json index 5095447..7dec538 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@types/react-dom": "^19", "autoprefixer": "^10.4.19", "cross-env": "^10.1.0", - "eslint": "^9", + "eslint": "^9.39.2", "eslint-config-next": "16.0.10", "postcss": "^8.4.31", "tailwindcss": "^3.4.17", diff --git a/package.json b/package.json index 4d242ac..3760ab8 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@types/react-dom": "^19", "autoprefixer": "^10.4.19", "cross-env": "^10.1.0", - "eslint": "^9", + "eslint": "^9.39.2", "eslint-config-next": "16.0.10", "postcss": "^8.4.31", "tailwindcss": "^3.4.17", diff --git a/public/sw.js b/public/sw.js index d7b0292..1578bf5 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let c={};const r=e=>a(e,t),d={module:{uri:t},exports:c,require:r};s[t]=Promise.all(n.map(e=>d[e]||r(e))).then(e=>(i(...e),c))}}define(["./workbox-f1770938"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/OY9BTVDo_gtYruE727h3x/_buildManifest.js",revision:"745555c29d619878e260d8023b7014c1"},{url:"/_next/static/OY9BTVDo_gtYruE727h3x/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/4bd1b696-deba172d32c79f82.js",revision:"deba172d32c79f82"},{url:"/_next/static/chunks/928-ad31bd01446f2472.js",revision:"ad31bd01446f2472"},{url:"/_next/static/chunks/973-e432f2a884511a8e.js",revision:"e432f2a884511a8e"},{url:"/_next/static/chunks/app/_global-error/page-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/app/_not-found/page-f7e9e5d73062cbe5.js",revision:"f7e9e5d73062cbe5"},{url:"/_next/static/chunks/app/layout-83a3f45ebc4f840c.js",revision:"83a3f45ebc4f840c"},{url:"/_next/static/chunks/app/legal/page-fe212f5a92cb34b9.js",revision:"fe212f5a92cb34b9"},{url:"/_next/static/chunks/app/page-bcc90e8c733e3c93.js",revision:"bcc90e8c733e3c93"},{url:"/_next/static/chunks/framework-4e51298db41fcfd4.js",revision:"4e51298db41fcfd4"},{url:"/_next/static/chunks/main-app-abcb25e568d7cd6e.js",revision:"abcb25e568d7cd6e"},{url:"/_next/static/chunks/main-c9d818b60410a49e.js",revision:"c9d818b60410a49e"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-1dc6a36b040319f2.js",revision:"1dc6a36b040319f2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-07bf70a7cee35489.js",revision:"07bf70a7cee35489"},{url:"/_next/static/css/bc594687c1a0ae55.css",revision:"bc594687c1a0ae55"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/apple-touch-icon.png",revision:"b73ace5d08ca50ae4d207ae49aa324a7"},{url:"/assets/screenshot-calculator.png",revision:"b5fb22478199d5d0a0567d6c3bd2c05d"},{url:"/assets/screenshot-hangar.png",revision:"ebf24315ea400345ca95724c2f38067f"},{url:"/assets/screenshot-report.png",revision:"153a01d7958719252c31e1a52830eb0f"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192.png",revision:"4cc4ba11db593a4f3899fc2c628e4bfb"},{url:"/icon-512.png",revision:"808fb568cffbb81a989f7e187df3fcc9"},{url:"/manifest.json",revision:"4b7e0c73a2888962193970e2f5bd7149"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/swe-worker-5c72df51bb1f6ee0.js",revision:"76fdd3369f623a3edcf74ce2200bfdd0"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\/_next\/static.+\.js$/i,new e.CacheFirst({cacheName:"next-static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4|webm)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:48,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e,url:{pathname:s}})=>!(!e||s.startsWith("/api/auth/callback")||!s.startsWith("/api/")),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:s},sameOrigin:a})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&a&&!s.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc-prefetch",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:s},sameOrigin:a})=>"1"===e.headers.get("RSC")&&a&&!s.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:{pathname:e},sameOrigin:s})=>s&&!e.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e})=>!e,new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET"),self.__WB_DISABLE_DEV_LOGS=!0}); +if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let c={};const r=e=>a(e,t),d={module:{uri:t},exports:c,require:r};s[t]=Promise.all(n.map(e=>d[e]||r(e))).then(e=>(i(...e),c))}}define(["./workbox-f1770938"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/chunks/4bd1b696-deba172d32c79f82.js",revision:"deba172d32c79f82"},{url:"/_next/static/chunks/928-34428627dfa9fa40.js",revision:"34428627dfa9fa40"},{url:"/_next/static/chunks/973-e432f2a884511a8e.js",revision:"e432f2a884511a8e"},{url:"/_next/static/chunks/app/_global-error/page-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/app/_not-found/page-f7e9e5d73062cbe5.js",revision:"f7e9e5d73062cbe5"},{url:"/_next/static/chunks/app/layout-83a3f45ebc4f840c.js",revision:"83a3f45ebc4f840c"},{url:"/_next/static/chunks/app/legal/page-fe212f5a92cb34b9.js",revision:"fe212f5a92cb34b9"},{url:"/_next/static/chunks/app/page-eec9a170c2819614.js",revision:"eec9a170c2819614"},{url:"/_next/static/chunks/framework-4e51298db41fcfd4.js",revision:"4e51298db41fcfd4"},{url:"/_next/static/chunks/main-2682d92e5634a569.js",revision:"2682d92e5634a569"},{url:"/_next/static/chunks/main-app-abcb25e568d7cd6e.js",revision:"abcb25e568d7cd6e"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-1dc6a36b040319f2.js",revision:"1dc6a36b040319f2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-07bf70a7cee35489.js",revision:"07bf70a7cee35489"},{url:"/_next/static/css/7857fc8dde1f0206.css",revision:"7857fc8dde1f0206"},{url:"/_next/static/jJ1Bsl0_rqcCnHxawFS6Y/_buildManifest.js",revision:"745555c29d619878e260d8023b7014c1"},{url:"/_next/static/jJ1Bsl0_rqcCnHxawFS6Y/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/apple-touch-icon.png",revision:"b73ace5d08ca50ae4d207ae49aa324a7"},{url:"/assets/screenshot-calculator.png",revision:"b5fb22478199d5d0a0567d6c3bd2c05d"},{url:"/assets/screenshot-hangar.png",revision:"ebf24315ea400345ca95724c2f38067f"},{url:"/assets/screenshot-report.png",revision:"153a01d7958719252c31e1a52830eb0f"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192.png",revision:"4cc4ba11db593a4f3899fc2c628e4bfb"},{url:"/icon-512.png",revision:"808fb568cffbb81a989f7e187df3fcc9"},{url:"/manifest.json",revision:"4b7e0c73a2888962193970e2f5bd7149"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/swe-worker-5c72df51bb1f6ee0.js",revision:"76fdd3369f623a3edcf74ce2200bfdd0"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\/_next\/static.+\.js$/i,new e.CacheFirst({cacheName:"next-static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4|webm)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:48,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e,url:{pathname:s}})=>!(!e||s.startsWith("/api/auth/callback")||!s.startsWith("/api/")),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:s},sameOrigin:a})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&a&&!s.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc-prefetch",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:s},sameOrigin:a})=>"1"===e.headers.get("RSC")&&a&&!s.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:{pathname:e},sameOrigin:s})=>s&&!e.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e})=>!e,new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET"),self.__WB_DISABLE_DEV_LOGS=!0}); diff --git a/utils/validation.ts b/utils/validation.ts new file mode 100644 index 0000000..814204f --- /dev/null +++ b/utils/validation.ts @@ -0,0 +1,65 @@ +import { Aircraft, SavedAircraft } from "../data/aircraft"; + +/** + * Validates if the input is a valid SavedAircraft object. + */ +function isValidSavedAircraft(obj: any): obj is SavedAircraft { + if (typeof obj !== 'object' || obj === null) return false; + + // Check required string fields + if (typeof obj.id !== 'string') return false; + if (typeof obj.model !== 'string') return false; + // registration is optional in base Aircraft but usually present in SavedAircraft + if (obj.registration !== undefined && typeof obj.registration !== 'string') return false; + + // Check required number fields + if (typeof obj.emptyWeight !== 'number' || !Number.isFinite(obj.emptyWeight)) return false; + if (typeof obj.emptyArm !== 'number' || !Number.isFinite(obj.emptyArm)) return false; + + // Check stations array + if (!Array.isArray(obj.stations)) return false; + for (const station of obj.stations) { + if (typeof station !== 'object' || station === null) return false; + if (typeof station.id !== 'string') return false; + if (typeof station.name !== 'string') return false; + if (typeof station.arm !== 'number' || !Number.isFinite(station.arm)) return false; + } + + // Check envelope array + if (!Array.isArray(obj.envelope)) return false; + for (const point of obj.envelope) { + if (typeof point !== 'object' || point === null) return false; + if (typeof point.cg !== 'number' || !Number.isFinite(point.cg)) return false; + if (typeof point.weight !== 'number' || !Number.isFinite(point.weight)) return false; + } + + // Check optional fields if present + if (obj.isCustomPlane !== undefined && typeof obj.isCustomPlane !== 'boolean') return false; + + if (obj.savedArmOverrides !== undefined) { + if (typeof obj.savedArmOverrides !== 'object' || obj.savedArmOverrides === null) return false; + for (const key in obj.savedArmOverrides) { + if (typeof obj.savedArmOverrides[key] !== 'number' || !Number.isFinite(obj.savedArmOverrides[key])) return false; + } + } + + return true; +} + +/** + * Validates if the input is an array of SavedAircraft objects. + * Use this when loading fleet data from LocalStorage or file import. + */ +export function validateFleetData(data: any): SavedAircraft[] | null { + if (!Array.isArray(data)) return null; + + // Filter out any invalid aircraft objects to ensure robustness + // Or return null if strictly enforcing integrity. + // Defense in depth: Let's clean the data by filtering valid items only. + const validFleet = data.filter(isValidSavedAircraft); + + // If the array was not empty but no valid items found, it might be the wrong format completely. + if (data.length > 0 && validFleet.length === 0) return null; + + return validFleet; +}