Add ADS-B, AIS, and ALPR map layers with live CoT streaming.
PR / lint (pull_request) Failing after 31s
PR / test (pull_request) Successful in 45s
PR / docker-build (pull_request) Successful in 1m3s
PR / e2e (pull_request) Successful in 1m33s

Ingest aircraft and vessel tracks via OSINT feeds and tactical CoT, expose viewport-filtered SSE to the map, and add an OSM ALPR layer with tiled caching and performant marker sync.
This commit is contained in:
Madison Grubb
2026-06-24 16:24:41 -04:00
parent a6b87305a1
commit aa8a0bd83f
64 changed files with 5761 additions and 2119 deletions
+27 -10
View File
@@ -7,6 +7,7 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { spawn, execSync } from 'node:child_process'
import { connect } from 'node:tls'
import { createServer } from 'node:net'
import { existsSync, mkdirSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -21,11 +22,21 @@ const devCertsDir = join(projectRoot, '.dev-certs')
const devKey = join(devCertsDir, 'key.pem')
const devCert = join(devCertsDir, 'cert.pem')
const API_PORT = 3000
const COT_PORT = 8089
const COT_AUTH_USER = 'test'
const COT_AUTH_PASS = 'test'
function getFreePort() {
return new Promise((resolve, reject) => {
const server = createServer()
server.listen(0, () => {
const address = server.address()
const port = typeof address === 'object' && address ? address.port : 0
server.close(() => resolve(port))
})
server.on('error', reject)
})
}
function ensureDevCerts() {
if (existsSync(devKey) && existsSync(devCert)) return
mkdirSync(devCertsDir, { recursive: true })
@@ -37,12 +48,12 @@ function ensureDevCerts() {
const FETCH_TIMEOUT_MS = 5000
async function waitForHealth(timeoutMs = 90000) {
async function waitForHealth(apiPort, timeoutMs = 90000) {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
for (const protocol of ['https', 'http']) {
try {
const baseURL = `${protocol}://localhost:${API_PORT}`
const baseURL = `${protocol}://localhost:${apiPort}`
const ctrl = new AbortController()
const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS)
const res = await fetch(`${baseURL}/health`, { method: 'GET', signal: ctrl.signal })
@@ -55,16 +66,20 @@ async function waitForHealth(timeoutMs = 90000) {
}
await new Promise(r => setTimeout(r, 1000))
}
throw new Error(`Health not OK on ${API_PORT} within ${timeoutMs}ms`)
throw new Error(`Health not OK on ${apiPort} within ${timeoutMs}ms`)
}
describe('Server and CoT integration', () => {
const testState = {
serverProcess: null,
apiPort: 0,
cotPort: 0,
}
beforeAll(async () => {
ensureDevCerts()
testState.apiPort = await getFreePort()
testState.cotPort = await getFreePort()
const serverPath = join(projectRoot, '.output', 'server', 'index.mjs')
if (!existsSync(serverPath)) {
execSync('npm run build', { cwd: projectRoot, stdio: 'pipe' })
@@ -72,6 +87,8 @@ describe('Server and CoT integration', () => {
const dbPath = join(tmpdir(), `kestrelos-it-${process.pid}-${Date.now()}.db`)
const env = {
...process.env,
PORT: String(testState.apiPort),
COT_PORT: String(testState.cotPort),
DB_PATH: dbPath,
BOOTSTRAP_EMAIL: COT_AUTH_USER,
BOOTSTRAP_PASSWORD: COT_AUTH_PASS,
@@ -83,7 +100,7 @@ describe('Server and CoT integration', () => {
})
testState.serverProcess.stdout?.on('data', d => process.stdout.write(d))
testState.serverProcess.stderr?.on('data', d => process.stderr.write(d))
await waitForHealth(90000)
await waitForHealth(testState.apiPort, 90000)
}, 120000)
afterAll(() => {
@@ -92,11 +109,11 @@ describe('Server and CoT integration', () => {
}
})
it('serves health on port 3000', async () => {
it('serves health on the configured API port', async () => {
const tryProtocols = async (protocols) => {
if (protocols.length === 0) throw new Error('No protocol succeeded')
try {
const res = await fetch(`${protocols[0]}://localhost:${API_PORT}/health`, { method: 'GET', headers: { Accept: 'application/json' } })
const res = await fetch(`${protocols[0]}://localhost:${testState.apiPort}/health`, { method: 'GET', headers: { Accept: 'application/json' } })
if (res?.ok) return res
return tryProtocols(protocols.slice(1))
}
@@ -112,10 +129,10 @@ describe('Server and CoT integration', () => {
expect(body.endpoints).toHaveProperty('ready', '/health/ready')
})
it('CoT on 8089: TAK client auth with username/password succeeds (socket stays open)', async () => {
it('CoT: TAK client auth with username/password succeeds (socket stays open)', async () => {
const payload = buildAuthCotXml({ username: COT_AUTH_USER, password: COT_AUTH_PASS })
const socket = await new Promise((resolve, reject) => {
const s = connect(COT_PORT, '127.0.0.1', { rejectUnauthorized: false }, () => {
const s = connect(testState.cotPort, '127.0.0.1', { rejectUnauthorized: false }, () => {
s.write(payload, () => resolve(s))
})
s.on('error', reject)
+23 -24
View File
@@ -1,27 +1,24 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { SHUTDOWN_TIMEOUT_MS } from '../../server/utils/constants.js'
import { registerCleanup, graceful, initShutdownHandlers, clearCleanup } from '../../server/utils/shutdown.js'
describe('shutdown integration', () => {
const testState = {
originalExit: null,
exitCalls: [],
originalOn: null,
}
/** @type {import('vitest').MockInstance} */
let exitSpy
/** @type {typeof process.on} */
let originalOn
beforeEach(() => {
clearCleanup()
testState.exitCalls = []
testState.originalExit = process.exit
process.exit = vi.fn((code) => {
testState.exitCalls.push(code)
})
testState.originalOn = process.on
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
originalOn = process.on
})
afterEach(() => {
process.exit = testState.originalExit
process.on = testState.originalOn
exitSpy.mockRestore()
process.on = originalOn
clearCleanup()
vi.useRealTimers()
})
it('initializes signal handlers', () => {
@@ -32,7 +29,6 @@ describe('shutdown integration', () => {
initShutdownHandlers()
expect(process.on).toHaveBeenCalledWith('SIGTERM', expect.any(Function))
expect(process.on).toHaveBeenCalledWith('SIGINT', expect.any(Function))
process.on = testState.originalOn
})
it('signal handler calls graceful', async () => {
@@ -43,9 +39,10 @@ describe('shutdown integration', () => {
initShutdownHandlers()
const sigtermHandler = handlers.SIGTERM
expect(sigtermHandler).toBeDefined()
await sigtermHandler()
expect(testState.exitCalls.length).toBeGreaterThan(0)
process.on = testState.originalOn
sigtermHandler()
await vi.waitFor(() => {
expect(exitSpy).toHaveBeenCalled()
})
})
it('signal handler handles graceful error', async () => {
@@ -59,18 +56,20 @@ describe('shutdown integration', () => {
registerCleanup(async () => {
throw new Error('Force error')
})
await sigintHandler()
expect(testState.exitCalls.length).toBeGreaterThan(0)
process.on = testState.originalOn
sigintHandler()
await vi.waitFor(() => {
expect(exitSpy).toHaveBeenCalled()
})
})
it('covers timeout path in graceful', async () => {
vi.useFakeTimers()
registerCleanup(async () => {
await new Promise(resolve => setTimeout(resolve, 40000))
await new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS + 5_000))
})
graceful()
await new Promise(resolve => setTimeout(resolve, 100))
expect(testState.exitCalls.length).toBeGreaterThan(0)
await vi.advanceTimersByTimeAsync(SHUTDOWN_TIMEOUT_MS + 1)
expect(exitSpy).toHaveBeenCalledWith(1)
})
it('covers graceful catch block', async () => {
@@ -78,6 +77,6 @@ describe('shutdown integration', () => {
throw new Error('Test error')
})
await graceful()
expect(testState.exitCalls.length).toBeGreaterThan(0)
expect(exitSpy).toHaveBeenCalled()
})
})