From e3cdff60538888562095d2105da33f149e3cf6ca Mon Sep 17 00:00:00 2001 From: Cyclonite01 Date: Mon, 6 Jul 2026 20:10:11 -0400 Subject: [PATCH 1/2] fix(wigle): return numeric epoch times for Explorer WiGLE observations Cast EXTRACT(EPOCH FROM ...) * 1000 to float8 for both WiGLE observation queries to ensure numeric timestamps are returned to client for proper temporal rendering in Geospatial Explorer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 +- client/src/components/wigle/mapHandlers.ts | 58 +------ docs/metrics/lines-of-code.md | 10 +- server/src/api/routes/v1/wigle/database.ts | 8 +- server/src/services/observationService.ts | 4 +- tests/e2e/README.md | 60 +++++++ tests/e2e/wigleTimestampDiag.spec.ts | 182 +++++++++++++++++++++ 7 files changed, 259 insertions(+), 68 deletions(-) create mode 100644 tests/e2e/wigleTimestampDiag.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f17766c8b..4fcb3b5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.0.0 (2026-07-06) +# 1.0.0 (2026-07-08) ### Bug Fixes @@ -1013,6 +1013,7 @@ - **wigle:** restore tooltip on WiGLE v2 and v3 map point click ([fe66827](https://github.com/cyclonite69/shadowcheck-web/commit/fe66827c63b4a1b01d3d7c65fee45881c8163bb2)) - **wigle:** restore v2/v3 toggle refresh and add encryption filtering ([8016359](https://github.com/cyclonite69/shadowcheck-web/commit/801635931a8d15d215c505de16192552248a4b10)) - **wigle:** restore WiGLE-only and local-match badges on map tooltips ([4b84f0d](https://github.com/cyclonite69/shadowcheck-web/commit/4b84f0d54d2e941b9d2e3cfcc1d09f24b4080060)) +- **wigle:** return numeric epoch times for Explorer WiGLE observations ([fa848cf](https://github.com/cyclonite69/shadowcheck-web/commit/fa848cf9a3fa53acec0c19383b8b87b48b7fe9af)) - **wigle:** run import loop in background to prevent HTTP 504 timeouts ([4c7aba3](https://github.com/cyclonite69/shadowcheck-web/commit/4c7aba3bf255ea38b4773c2dae80b1daec84e496)) - **wigle:** seed field data bssids when wigle datasets are off ([3e33420](https://github.com/cyclonite69/shadowcheck-web/commit/3e33420b848c91033d5c7008f51b2b1e648eadbf)) - **wigle:** serve cached detail before live lookup ([e86cb52](https://github.com/cyclonite69/shadowcheck-web/commit/e86cb52ef3506ab988727f67e7d9fe8d349bdb1c)) @@ -1474,4 +1475,4 @@ - return all agencies within radius (natural geographic clusters) ([62318fd](https://github.com/cyclonite69/shadowcheck-web/commit/62318fd0b4aa925f3822b1fd06784954cb214ae3)) - **tooltip:** remove kepler optimization—causes field depopulation ([9730707](https://github.com/cyclonite69/shadowcheck-web/commit/9730707570591d725e8ff89190609a971b5274cb)) - **wigle:** remove backend aggregation — restore Mapbox client-side clustering ([f37110b](https://github.com/cyclonite69/shadowcheck-web/commit/f37110bc744a4c164cf363527ae96a0bbd639a93)) -- **wigle:** undo speculative timestamp fixes; instrument enrichment pipeline ([9ee1030](https://github.com/cyclonite69/shadowcheck-web/commit/9ee10303025f03c53c0aa739ab7467f82962de7d)) +- **wigle:** undo speculative timestamp fixes; instrument enrichment pipeline ([a9313ca](https://github.com/cyclonite69/shadowcheck-web/commit/a9313cae6d8c52a7e790540952125e1bcce5159a)) diff --git a/client/src/components/wigle/mapHandlers.ts b/client/src/components/wigle/mapHandlers.ts index 146a69644..11c904bc7 100644 --- a/client/src/components/wigle/mapHandlers.ts +++ b/client/src/components/wigle/mapHandlers.ts @@ -23,6 +23,7 @@ export const attachClickHandlers = ( if (!props || !e.lngLat) return; const netid = String(props.netid || props.bssid || ''); + const featureData: Record = { ...(props as Record), netid: String(props.netid || props.bssid || ''), @@ -32,28 +33,7 @@ export const attachClickHandlers = ( wigle_source: props.wigle_source === 'wigle-v3' ? 'wigle-v3' : 'wigle-v2', }; - // ── INSTRUMENTATION: Stage 1 — feature properties at click ────────────── - console.log('[popup:1:feature]', netid, { - lasttime: featureData.lasttime, - observed_at: featureData.observed_at, - first_seen: featureData.first_seen, - last_seen: featureData.last_seen, - wigle_v3_first_seen: featureData.wigle_v3_first_seen, - wigle_v3_last_seen: featureData.wigle_v3_last_seen, - wigle_source: featureData.wigle_source, - }); - const initialNormalized = normalizeTooltipData(featureData, [e.lngLat.lng, e.lngLat.lat]); - - // ── INSTRUMENTATION: Stage 2 — normalizer output for initial render ────── - console.log('[popup:2:initial-normalized]', netid, { - time: initialNormalized.time, - first_seen: initialNormalized.first_seen, - last_seen: initialNormalized.last_seen, - timespan_days: initialNormalized.timespan_days, - wigle_observation_count: initialNormalized.wigle_observation_count, - }); - const initialHTML = renderNetworkTooltip(initialNormalized); const anchor = getPopupAnchor(map, e.lngLat, initialHTML); @@ -77,15 +57,6 @@ export const attachClickHandlers = ( const { wigle, localLinkage } = pageResponse; - // ── INSTRUMENTATION: Stage 3 — raw enrichment API response ───────── - console.log('[popup:3:api-response]', netid, { - wigle_v3_first_seen: wigle.wigle_v3_first_seen, - wigle_v3_last_seen: wigle.wigle_v3_last_seen, - wigle_v3_observation_count: wigle.wigle_v3_observation_count, - wigle_v2_lasttime: wigle.wigle_v2_lasttime, - wigle_v2_lastupdt: wigle.wigle_v2_lastupdt, - wigle_source: wigle.wigle_source, - }); // Build merged object: WiGLE-truth fields from structured `wigle` section only. // Local linkage maps to the existing flat fields the normalizer already reads. const mergedData: Record = { @@ -148,33 +119,10 @@ export const attachClickHandlers = ( local_last_seen: localLinkage.local_last_seen, }; - // ── INSTRUMENTATION: Stage 4 — mergedData before normalizer ──────── - console.log('[popup:4:merged]', netid, { - lasttime: mergedData.lasttime, - observed_at: mergedData.observed_at, - wigle_v3_first_seen: mergedData.wigle_v3_first_seen, - wigle_v3_last_seen: mergedData.wigle_v3_last_seen, - wigle_v2_lasttime: mergedData.wigle_v2_lasttime, - timespan_days: mergedData.timespan_days, - wigle_v3_observation_count: mergedData.wigle_v3_observation_count, - }); - - const enrichedNormalized = normalizeTooltipData(mergedData); - - // ── INSTRUMENTATION: Stage 5 — normalizer output for enriched render - console.log('[popup:5:enriched-normalized]', netid, { - time: enrichedNormalized.time, - first_seen: enrichedNormalized.first_seen, - last_seen: enrichedNormalized.last_seen, - timespan_days: enrichedNormalized.timespan_days, - wigle_observation_count: enrichedNormalized.wigle_observation_count, - }); - - popup.setHTML(renderNetworkTooltip(enrichedNormalized)); + popup.setHTML(renderNetworkTooltip(normalizeTooltipData(mergedData))); }) .catch((err) => { - // ── INSTRUMENTATION: enrichment request failure ──────────────────── - console.warn('[popup:enrichment-failed]', netid, err); + console.warn('[wigle-tooltip] enrichment fetch failed', err); }); } diff --git a/docs/metrics/lines-of-code.md b/docs/metrics/lines-of-code.md index c82b7a04e..0eafe3ee0 100644 --- a/docs/metrics/lines-of-code.md +++ b/docs/metrics/lines-of-code.md @@ -1,15 +1,15 @@ # LOC Metrics -_Generated: Mon Jul 6 03:13:58 PM EDT 2026 | SHA: 9ee10303_ +_Generated: Wed Jul 8 09:10:47 AM EDT 2026 | SHA: fa848cf9_ -| cloc | github.com/AlDanial/cloc v 2.06 T=4.00 s (461.4 files/s, 161472.9 lines/s) | +| cloc | github.com/AlDanial/cloc v 2.06 T=4.88 s (379.2 files/s, 132489.5 lines/s) | | ---- | -------------------------------------------------------------------------- | | Language | files | blank | comment | code | | :--------- | -------: | -------: | -------: | -------: | -| SQL | 338 | 4489 | 6203 | 414835 | -| TypeScript | 1474 | 24231 | 8284 | 182592 | +| SQL | 340 | 4533 | 6245 | 414867 | +| TypeScript | 1476 | 24275 | 8324 | 182854 | | JavaScript | 32 | 486 | 237 | 3908 | | CSS | 1 | 47 | 27 | 276 | | -------- | -------- | -------- | -------- | -------- | -| SUM: | 1845 | 29253 | 14751 | 601611 | +| SUM: | 1849 | 29341 | 14833 | 601905 | diff --git a/server/src/api/routes/v1/wigle/database.ts b/server/src/api/routes/v1/wigle/database.ts index 16c386317..d898a10c5 100644 --- a/server/src/api/routes/v1/wigle/database.ts +++ b/server/src/api/routes/v1/wigle/database.ts @@ -70,15 +70,15 @@ const validateWigleNetworksQuery = validateQuery({ * the wigle_networks_enriched view for v2-only imports. */ router.get( - '/page/network/:netid', + '/page/network/:bssid', macParamMiddleware, asyncHandler(async (req: Request, res: Response) => { - const { netid } = req.params; + const { bssid } = req.params; // Try MV first (single-row read); fall back to live 4-query fan-out if MV // is unavailable (pre-migration deployment) or returns no row. - let network = await wigleService.getWiglePageNetworkFromMv(netid); + let network = await wigleService.getWiglePageNetworkFromMv(bssid); if (!network) { - network = await wigleService.getWiglePageNetwork(netid); + network = await wigleService.getWiglePageNetwork(bssid); } if (!network) { return res.status(404).json({ error: 'Network not found in WiGLE database' }); diff --git a/server/src/services/observationService.ts b/server/src/services/observationService.ts index b30805b0d..8a98ba615 100644 --- a/server/src/services/observationService.ts +++ b/server/src/services/observationService.ts @@ -92,7 +92,7 @@ export async function getWigleObservationsByBSSID(bssid: string): Promise FROM app.wigle_v3_observations w WHERE UPPER(w.netid) = $1 AND w.latitude IS NOT NULL AND w.longitude IS NOT NULL ) - SELECT we.bssid, we.lat, we.lon, EXTRACT(EPOCH FROM we.time) * 1000 as time, + SELECT we.bssid, we.lat, we.lon, (EXTRACT(EPOCH FROM we.time) * 1000)::float8 as time, we.level, we.ssid, we.frequency, we.channel, we.encryption, we.altitude, we.accuracy, we.is_matched, CASE @@ -149,7 +149,7 @@ export async function getWigleObservationsBatch(bssids: string[]): Promise npx playwright test tests/e2e/wigleTimestampDiag.spec.ts --reporter=list + */ + +import { test, expect } from '@playwright/test'; + +// Known dates matching the wigleTooltipTemporal mock — deterministic +const FIRST_SEEN = '2023-02-17T08:00:00.000Z'; +const LAST_SEEN = '2026-02-17T08:00:00.000Z'; +const MOCK_BSSID = 'A4:43:8C:64:2A:43'; +const MOCK_LNG = -83.696; +const MOCK_LAT = 43.023; + +const MOCK_NETWORK_RESPONSE = { + wigle: { + bssid: MOCK_BSSID, + ssid: 'DIAG-V3', + name: 'DIAG-V3', + type: 'W', + encryption: '[WPA2-PSK-CCMP][ESS]', + channel: 6, + frequency: 2437, + qos: 5, + comment: null, + wigle_source: 'wigle-v3', + wigle_v2_firsttime: null, + wigle_v2_lasttime: null, + wigle_v2_lastupdt: null, + wigle_v2_trilat: null, + wigle_v2_trilong: null, + wigle_v2_city: null, + wigle_v2_region: null, + wigle_v2_road: null, + wigle_v2_housenumber: null, + has_wigle_v2_record: false, + wigle_v3_first_seen: FIRST_SEEN, + wigle_v3_last_seen: LAST_SEEN, + wigle_v3_observation_count: 42, + wigle_v3_centroid_lat: MOCK_LAT, + wigle_v3_centroid_lon: MOCK_LNG, + wigle_v3_spread_m: 50, + has_wigle_v3_observations: true, + display_lat: MOCK_LAT, + display_lon: MOCK_LNG, + display_coordinate_source: 'wigle-v3-centroid', + manufacturer: 'Cisco', + public_nonstationary_flag: false, + public_ssid_variant_flag: false, + wigle_precision_warning: false, + recent_ssid: 'DIAG-V3', + recent_channel: 6, + recent_frequency: 2437, + recent_accuracy: 8, + geocoded_address: '1 Diag St, Test, MI', + }, + localLinkage: { + has_local_match: false, + local_observation_count: 0, + local_first_seen: null, + local_last_seen: null, + }, +}; + +test('capture popup pipeline boundary logs for a v3 network', async ({ page }) => { + // Collect every [popup:*] console message in order + const logs: { tag: string; traceId: string; data: any }[] = []; + + page.on('console', (msg) => { + const text = msg.text(); + if (!text.includes('[popup:')) return; + // Playwright serialises structured console.log args — grab them + const args = msg.args(); + // tag is first arg, traceId is second, data object is third + Promise.all(args.map((a) => a.jsonValue().catch(() => String(a)))).then((vals) => { + logs.push({ + tag: String(vals[0] ?? ''), + traceId: String(vals[1] ?? ''), + data: vals[2] ?? {}, + }); + }); + }); + + page.on('pageerror', (err) => console.error('[pageerror]', err.message)); + + // Intercept enrichment so we get deterministic timestamps regardless of DB state + await page.route('**/api/wigle/page/network/**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MOCK_NETWORK_RESPONSE), + }); + }); + + await page.goto('/wigle'); + + await page.waitForFunction(() => !!(window as any).__wigleMapInstance, { timeout: 15000 }); + await page.waitForFunction(() => (window as any).__wigleMapInstance?.isStyleLoaded?.(), { + timeout: 15000, + }); + await page.waitForFunction(() => typeof (window as any).__wigleHandleUnclustered === 'function', { + timeout: 10000, + }); + + // Fire the handler with a synthetic v3 feature — no canvas click, no zoom/cluster dependency + await page.evaluate( + ({ lng, lat, bssid }) => { + (window as any).__wigleHandleUnclustered({ + features: [ + { + type: 'Feature', + geometry: { type: 'Point', coordinates: [lng, lat] }, + properties: { + bssid, + netid: bssid, + ssid: 'DIAG-V3', + trilat: lat, + trilong: lng, + wigle_source: 'wigle-v3', + // No lasttime / observed_at on the feature — simulates the real v3 case + }, + }, + ], + lngLat: { lng, lat }, + }); + }, + { lng: MOCK_LNG, lat: MOCK_LAT, bssid: MOCK_BSSID } + ); + + // Wait for popup to appear + await expect(page.locator('.sc-popup')).toBeVisible({ timeout: 8000 }); + + // Wait for enrichment: 42 obs count only appears after mock resolves + await expect(page.locator('.sc-popup')).toContainText('42', { timeout: 8000 }); + + // Let async console.log promises resolve + await page.waitForTimeout(600); + + // Expand the Timestamps
section if present + const details = page.locator('.sc-popup details'); + if (await details.isVisible({ timeout: 2000 }).catch(() => false)) { + await details.click(); + await page.waitForTimeout(200); + } + + // Read the rendered SEEN cell text from the DOM + const seenDomText = await page.evaluate(() => { + const popup = document.querySelector('.sc-popup'); + if (!popup) return null; + for (const div of Array.from(popup.querySelectorAll('div'))) { + if (div.textContent?.trim() === 'Seen') { + const sibling = div.parentElement?.querySelector('div:last-child'); + return sibling?.textContent?.trim() ?? null; + } + } + return null; + }); + + console.log('\n=== DOM RESULT ==='); + console.log('SEEN cell text in rendered popup:', JSON.stringify(seenDomText)); + + const enrichmentFailed = logs.find( + (l) => + l.tag === '[popup:enrichment-failed]' || + (typeof l.tag === 'string' && l.tag.includes('enrichment')) + ); + + if (enrichmentFailed) { + console.log('ENRICHMENT FAILED:', enrichmentFailed); + } + + // The SEEN cell must contain a real timestamp (not "—") — enrichment reached the server + // and returned valid wigle_v3_last_seen data. + expect(seenDomText, 'SEEN cell should contain a timestamp, not "—"').not.toBe('—'); + expect(seenDomText, 'SEEN cell should not be null').not.toBeNull(); + expect(enrichmentFailed, 'Enrichment must not fail').toBeUndefined(); +}); From 519bd4a41a55233d52e956c8403983593377db5c Mon Sep 17 00:00:00 2001 From: Cyclonite01 Date: Wed, 8 Jul 2026 15:39:55 -0400 Subject: [PATCH 2/2] fix(api): normalize observations endpoint timestamp type to float8 Cast EXTRACT(EPOCH FROM o.time) to float8 before multiplication to ensure numeric epoch millisecond timestamps in API responses. Update OpenAPI and API reference documentation to reflect numeric time field. Add focused test assertions for type correctness in service and integration layers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 ++- docs/API_REFERENCE.md | 3 +++ docs/metrics/lines-of-code.md | 8 ++++---- docs/openapi.yaml | 5 +++-- server/src/services/observationService.ts | 2 +- tests/integration/api/v1/observations.test.ts | 1 + tests/unit/observationService.test.ts | 2 ++ 7 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fcb3b5f9..35c4ad030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ - **api:** fail closed auth gate and secure exports ([b11cbf7](https://github.com/cyclonite69/shadowcheck-web/commit/b11cbf774205eaf9eaca7707d175620399e294de)) - **api:** make v2 filtered manufacturer fields schema-compatible and add regression tests ([2bd4e9f](https://github.com/cyclonite69/shadowcheck-web/commit/2bd4e9fa73a3b63c2f43cf48fc50ffdb0ffe26c9)) - **api:** normalize OBS radio_type values in radio filter expression ([d51065e](https://github.com/cyclonite69/shadowcheck-web/commit/d51065e965a92b76b2058cfbb6866bc759d8200e)) +- **api:** normalize observations endpoint timestamp type to float8 ([4a19bcc](https://github.com/cyclonite69/shadowcheck-web/commit/4a19bcc67251fec4c0729c3d6417c6493469068a)) - **api:** optimize filtered network-only queries and notes predicate ([782e59d](https://github.com/cyclonite69/shadowcheck-web/commit/782e59dfd342be44af8b8de63b1244c4dda7b52d)) - **api:** prevent filtered dashboard metrics fallback to zeros ([7a0c94f](https://github.com/cyclonite69/shadowcheck-web/commit/7a0c94fb5dc2c69343f8314b69fb5724641c8ca0)) - **api:** qualify sort columns with ne. alias to prevent 500 ambiguity errors ([dbf0db2](https://github.com/cyclonite69/shadowcheck-web/commit/dbf0db21f582d06af77b08362c47750a37ed9aec)) @@ -1013,7 +1014,7 @@ - **wigle:** restore tooltip on WiGLE v2 and v3 map point click ([fe66827](https://github.com/cyclonite69/shadowcheck-web/commit/fe66827c63b4a1b01d3d7c65fee45881c8163bb2)) - **wigle:** restore v2/v3 toggle refresh and add encryption filtering ([8016359](https://github.com/cyclonite69/shadowcheck-web/commit/801635931a8d15d215c505de16192552248a4b10)) - **wigle:** restore WiGLE-only and local-match badges on map tooltips ([4b84f0d](https://github.com/cyclonite69/shadowcheck-web/commit/4b84f0d54d2e941b9d2e3cfcc1d09f24b4080060)) -- **wigle:** return numeric epoch times for Explorer WiGLE observations ([fa848cf](https://github.com/cyclonite69/shadowcheck-web/commit/fa848cf9a3fa53acec0c19383b8b87b48b7fe9af)) +- **wigle:** return numeric epoch times for Explorer WiGLE observations ([e3cdff6](https://github.com/cyclonite69/shadowcheck-web/commit/e3cdff60538888562095d2105da33f149e3cf6ca)) - **wigle:** run import loop in background to prevent HTTP 504 timeouts ([4c7aba3](https://github.com/cyclonite69/shadowcheck-web/commit/4c7aba3bf255ea38b4773c2dae80b1daec84e496)) - **wigle:** seed field data bssids when wigle datasets are off ([3e33420](https://github.com/cyclonite69/shadowcheck-web/commit/3e33420b848c91033d5c7008f51b2b1e648eadbf)) - **wigle:** serve cached detail before live lookup ([e86cb52](https://github.com/cyclonite69/shadowcheck-web/commit/e86cb52ef3506ab988727f67e7d9fe8d349bdb1c)) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 33dab01be..d49498027 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -233,6 +233,9 @@ List networks with pagination and filtering. Get all observations for a network. +**Response:** `observations[].time` is a JavaScript-safe numeric epoch timestamp in +milliseconds. + ### GET /api/networks/search/:ssid Search by SSID. diff --git a/docs/metrics/lines-of-code.md b/docs/metrics/lines-of-code.md index 0eafe3ee0..e74e3a86f 100644 --- a/docs/metrics/lines-of-code.md +++ b/docs/metrics/lines-of-code.md @@ -1,15 +1,15 @@ # LOC Metrics -_Generated: Wed Jul 8 09:10:47 AM EDT 2026 | SHA: fa848cf9_ +_Generated: Wed Jul 8 03:40:32 PM EDT 2026 | SHA: 4a19bcc6_ -| cloc | github.com/AlDanial/cloc v 2.06 T=4.88 s (379.2 files/s, 132489.5 lines/s) | +| cloc | github.com/AlDanial/cloc v 2.06 T=4.29 s (431.1 files/s, 150656.1 lines/s) | | ---- | -------------------------------------------------------------------------- | | Language | files | blank | comment | code | | :--------- | -------: | -------: | -------: | -------: | | SQL | 340 | 4533 | 6245 | 414867 | -| TypeScript | 1476 | 24275 | 8324 | 182854 | +| TypeScript | 1475 | 24245 | 8301 | 182694 | | JavaScript | 32 | 486 | 237 | 3908 | | CSS | 1 | 47 | 27 | 276 | | -------- | -------- | -------- | -------- | -------- | -| SUM: | 1849 | 29341 | 14833 | 601905 | +| SUM: | 1848 | 29311 | 14810 | 601745 | diff --git a/docs/openapi.yaml b/docs/openapi.yaml index c24e8e43b..3ecc7c17b 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1175,8 +1175,9 @@ components: signal: type: integer time: - type: integer - format: int64 + type: number + format: double + description: Epoch timestamp in milliseconds, returned as a JSON number. acc: type: number alt: diff --git a/server/src/services/observationService.ts b/server/src/services/observationService.ts index 8a98ba615..1b58de4c8 100644 --- a/server/src/services/observationService.ts +++ b/server/src/services/observationService.ts @@ -32,7 +32,7 @@ export async function getObservationsByBSSID( const { rows } = await query( `SELECT ROW_NUMBER() OVER (ORDER BY o.time) as id, o.bssid, COALESCE(NULLIF(o.ssid, ''), '(hidden)') as ssid, o.radio_type as type, - o.lat, o.lon, o.level as signal, EXTRACT(EPOCH FROM o.time)::BIGINT * 1000 as time, + o.lat, o.lon, o.level as signal, (EXTRACT(EPOCH FROM o.time) * 1000)::float8 as time, COALESCE(o.accuracy, 3.79) as acc, o.altitude as alt, gc.address as geocoded_address, gc.city as geocoded_city, gc.state as geocoded_state, gc.poi_name as geocoded_poi_name, diff --git a/tests/integration/api/v1/observations.test.ts b/tests/integration/api/v1/observations.test.ts index 897139ff8..19c35988a 100644 --- a/tests/integration/api/v1/observations.test.ts +++ b/tests/integration/api/v1/observations.test.ts @@ -73,6 +73,7 @@ describe('Observations API v1', () => { expect(res.body.ok).toBe(true); expect(res.body.bssid).toBe(bssid); expect(res.body.observations).toEqual(mockObservations); + expect(typeof res.body.observations[0].time).toBe('number'); expect(res.body.home).toEqual(mockHome); }); diff --git a/tests/unit/observationService.test.ts b/tests/unit/observationService.test.ts index 17d9912e2..4c450440b 100644 --- a/tests/unit/observationService.test.ts +++ b/tests/unit/observationService.test.ts @@ -83,6 +83,8 @@ describe('Observation Service', () => { const result = await getObservationsByBSSID('AA:BB:CC:DD:EE:FF', -122.4194, 37.7749); expect(result).toEqual(mockRows); + const sql = (query as jest.Mock).mock.calls[0][0]; + expect(sql).toContain('(EXTRACT(EPOCH FROM o.time) * 1000)::float8 as time'); expect(query).toHaveBeenCalledWith(expect.stringContaining('ST_Distance'), [ -122.4194, 37.7749,