diff --git a/scripts/check-free-sources.mjs b/scripts/check-free-sources.mjs new file mode 100644 index 0000000..d1cacad --- /dev/null +++ b/scripts/check-free-sources.mjs @@ -0,0 +1,42 @@ +// Read-only source smoke check. Never prints or submits API credentials. +const checks = [ + ['Free configuration', '/api/free-providers'], + ['Place search', '/api/free-geocode?q=Reykjavik'], + ['Flights', '/api/opensky'], + ['Military flights', '/api/adsblol/mil'], + ['Satellites', '/api/celestrak/stations'], + ['Space missions', '/api/launches'], + ['Camera directory', '/api/cctv/sources'], + ['Traffic configuration', '/api/tomtom/status'], + ['Fire configuration', '/api/firms/status'], + ['Ships', '/api/ais-live'], + ['Weather', '/api/weather-effects?latitude=30.2672&longitude=-97.7431'], +]; +const results = []; +for (let i = 0; i < checks.length; i += 3) { + await Promise.all(checks.slice(i, i + 3).map(async ([name, path]) => { + const start = Date.now(); + try { + const response = await fetch(`http://localhost:4173${path}`, { signal: AbortSignal.timeout(30000) }); + const text = await response.text(); + const body = name === 'Satellites' && !text.trim().startsWith('{') + ? { results: text.trim().split('\n').filter(line => line.startsWith('1 ')) } + : JSON.parse(text); + const rows = body.states || body.ac || body.results || body.sources || body.vessels; + const result = { name, http: response.status, seconds: ((Date.now() - start) / 1000).toFixed(1), + ...(Array.isArray(rows) ? { count: rows.length } : {}), + ...(body.status ? { status: body.status } : {}), + ...(typeof body.hasKey === 'boolean' ? { hasKey: body.hasKey } : {}), + ...(body.place ? { place: body.place.label } : {}), + ...(path === '/api/free-providers' ? { configuration: body } : {}), + }; + results.push(result); + console.log(JSON.stringify(result)); + } catch (error) { + const result = { name, error: error.name === 'TimeoutError' ? 'timeout' : 'request failed' }; + results.push(result); + console.log(JSON.stringify(result)); + } + })); +} +if (results.some(r => r.error || (r.http >= 400 && r.name !== 'Ships'))) process.exitCode = 1; diff --git a/scripts/start-free.mjs b/scripts/start-free.mjs new file mode 100644 index 0000000..d2a23f5 --- /dev/null +++ b/scripts/start-free.mjs @@ -0,0 +1,21 @@ +import { spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +const root=fileURLToPath(new URL('../',import.meta.url)); +const vite=fileURLToPath(new URL('../node_modules/vite/bin/vite.js',import.meta.url)); +const [major,minor]=process.versions.node.split('.').map(Number); +if(!((major===24&&minor>=14)||major===26)){ + console.error('Install Node 24.14+ (24.x) or Node 26 before starting Gods Eye.'); + process.exit(1); +} +if(!existsSync(vite)){ + console.error('Run npm ci first to install dependencies.'); + process.exit(1); +} +const child=spawn(process.execPath,[vite,'--host','localhost','--port','4173','--strictPort','--open'],{ + cwd:root,stdio:'inherit', + env:{...process.env,GEV_FREE_ONLY:'1',GOOGLE_MAPS_API_KEY:'',OPENAI_API_KEY:'',HOST:'localhost',PORT:'4173'} +}); +child.on('error',()=>{console.error('Unable to start the local server.');process.exitCode=1;}); +child.on('exit',code=>{process.exitCode=code??1;}); +for(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>child.kill(signal)); diff --git a/scripts/verify-keyed-data.mjs b/scripts/verify-keyed-data.mjs new file mode 100644 index 0000000..21a9b7c --- /dev/null +++ b/scripts/verify-keyed-data.mjs @@ -0,0 +1,42 @@ +import { lonLatToTile } from '../src/data/tomtomTiles.js'; +import { decodeFlowTile } from '../src/data/flowTiles.js'; +const root = 'http://localhost:4173'; +const { x, y } = lonLatToTile(-97.7431, 30.2672, 12); +const checks = [ + ['Traffic tile', `/api/tomtom/flow/12/${x}/${y}.pbf`, 'tile'], + ['Fire detections', '/api/firms', 'fires'], + ['Ships', '/api/ais-live?maxRows=5000', 'ships'], + ['Camera health', '/api/cctv/health', 'cameras'], + ['Mapped installations', '/api/military-installations?south=30.15&west=-97.85&north=30.35&east=-97.65', 'mapped'], +]; +await Promise.all(checks.map(async ([name, path, kind]) => { + const start = Date.now(); + try { + const r = await fetch(root + path, { signal: AbortSignal.timeout(90000) }); + const bytes = new Uint8Array(await r.arrayBuffer()); + const result = { name, http: r.status, seconds: Math.round((Date.now() - start) / 1000) }; + if (kind === 'tile' && r.ok) { + result.bytes = bytes.length; + result.contentType = r.headers.get('content-type'); + result.cache = r.headers.get('x-tomtom-cache'); + result.decodedRoadSegments = decodeFlowTile(bytes, 12, x, y).length; + } else { + const body = JSON.parse(new TextDecoder().decode(bytes)); + if (kind === 'fires') Object.assign(result, { count: body.fires?.length, stale: body.stale, sourceCount: body.sources?.length }); + if (kind === 'ships') Object.assign(result, { count: body.rows?.length, status: body.status, lastMessageAt: body.lastMessageAt }); + if (kind === 'mapped') Object.assign(result, { count: body.elements?.length, status: body.status }); + if (kind === 'cameras') { + const cameras = Array.isArray(body.cameras) ? body.cameras : Object.values(body.cameras || {}); + result.observed = cameras.length; + result.statusCounts = cameras.reduce((counts, camera) => { + const status = camera.status || 'unknown'; counts[status] = (counts[status] || 0) + 1; return counts; + }, {}); + } + } + console.log(JSON.stringify(result)); + if (!r.ok) process.exitCode = 1; + } catch (e) { + console.log(JSON.stringify({ name, error: e.name === 'TimeoutError' ? 'timeout' : 'request failed' })); + process.exitCode = 1; + } +}));