mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
fix: use GPS utility for location, compact dashboard layout
Replace broken app.gps_state lookup with utils.gps.get_current_position() and return GPS metadata (fix quality, satellites, accuracy). Shrink location card to single-column with 200px globe, move System Info into row 2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -436,17 +436,21 @@ def _ensure_collector() -> None:
|
||||
def _get_observer_location() -> dict[str, Any]:
|
||||
"""Get observer location from GPS state or config defaults."""
|
||||
lat, lon, source = None, None, 'none'
|
||||
gps_meta: dict[str, Any] = {}
|
||||
|
||||
# Try GPS state from app module
|
||||
# Try GPS via utils.gps
|
||||
with contextlib.suppress(Exception):
|
||||
import app as app_module
|
||||
from utils.gps import get_current_position
|
||||
|
||||
gps_state = getattr(app_module, 'gps_state', None)
|
||||
if gps_state and isinstance(gps_state, dict):
|
||||
g_lat = gps_state.get('lat') or gps_state.get('latitude')
|
||||
g_lon = gps_state.get('lon') or gps_state.get('longitude')
|
||||
if g_lat is not None and g_lon is not None:
|
||||
lat, lon, source = float(g_lat), float(g_lon), 'gps'
|
||||
pos = get_current_position()
|
||||
if pos and pos.fix_quality >= 2:
|
||||
lat, lon, source = pos.latitude, pos.longitude, 'gps'
|
||||
gps_meta['fix_quality'] = pos.fix_quality
|
||||
gps_meta['satellites'] = pos.satellites
|
||||
if pos.epx is not None and pos.epy is not None:
|
||||
gps_meta['accuracy'] = round(max(pos.epx, pos.epy), 1)
|
||||
if pos.altitude is not None:
|
||||
gps_meta['altitude'] = round(pos.altitude, 1)
|
||||
|
||||
# Fall back to config defaults
|
||||
if lat is None:
|
||||
@@ -456,7 +460,10 @@ def _get_observer_location() -> dict[str, Any]:
|
||||
if DEFAULT_LATITUDE != 0.0 or DEFAULT_LONGITUDE != 0.0:
|
||||
lat, lon, source = DEFAULT_LATITUDE, DEFAULT_LONGITUDE, 'config'
|
||||
|
||||
return {'lat': lat, 'lon': lon, 'source': source}
|
||||
result: dict[str, Any] = {'lat': lat, 'lon': lon, 'source': source}
|
||||
if gps_meta:
|
||||
result['gps'] = gps_meta
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -282,16 +282,17 @@
|
||||
color: var(--accent-cyan, #00d4ff);
|
||||
}
|
||||
|
||||
/* Globe container */
|
||||
/* Globe container — compact vertical layout */
|
||||
.sys-location-inner {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sys-globe-wrap {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
flex-shrink: 0;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
@@ -300,10 +301,42 @@
|
||||
}
|
||||
|
||||
.sys-location-details {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* GPS status indicator */
|
||||
.sys-gps-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
color: var(--text-dim, #8888aa);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.sys-gps-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sys-gps-dot.fix-3d {
|
||||
background: var(--accent-green, #00ff88);
|
||||
box-shadow: 0 0 4px rgba(0, 255, 136, 0.4);
|
||||
}
|
||||
|
||||
.sys-gps-dot.fix-2d {
|
||||
background: var(--accent-yellow, #ffcc00);
|
||||
box-shadow: 0 0 4px rgba(255, 204, 0, 0.4);
|
||||
}
|
||||
|
||||
.sys-gps-dot.no-fix {
|
||||
background: var(--text-dim, #555);
|
||||
}
|
||||
|
||||
.sys-location-coords {
|
||||
@@ -515,13 +548,9 @@
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.sys-location-inner {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sys-globe-wrap {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.sys-process-grid {
|
||||
|
||||
@@ -390,14 +390,27 @@ const SystemHealth = (function () {
|
||||
// Globe container
|
||||
html += '<div class="sys-globe-wrap" id="sysGlobeContainer"></div>';
|
||||
|
||||
// Details column
|
||||
// Details below globe
|
||||
html += '<div class="sys-location-details">';
|
||||
|
||||
if (locationData && locationData.lat != null) {
|
||||
html += '<div class="sys-location-coords">' +
|
||||
locationData.lat.toFixed(4) + '°' + (locationData.lat >= 0 ? 'N' : 'S') + '<br>' +
|
||||
locationData.lat.toFixed(4) + '°' + (locationData.lat >= 0 ? 'N' : 'S') + ', ' +
|
||||
locationData.lon.toFixed(4) + '°' + (locationData.lon >= 0 ? 'E' : 'W') + '</div>';
|
||||
html += '<div class="sys-location-source">Source: ' + escHtml(locationData.source || 'unknown') + '</div>';
|
||||
|
||||
// GPS status indicator
|
||||
if (locationData.source === 'gps' && locationData.gps) {
|
||||
var gps = locationData.gps;
|
||||
var fixLabel = gps.fix_quality === 3 ? '3D Fix' : '2D Fix';
|
||||
var dotCls = gps.fix_quality === 3 ? 'fix-3d' : 'fix-2d';
|
||||
html += '<div class="sys-gps-status">' +
|
||||
'<span class="sys-gps-dot ' + dotCls + '"></span> ' + fixLabel;
|
||||
if (gps.satellites != null) html += ' · ' + gps.satellites + ' sats';
|
||||
if (gps.accuracy != null) html += ' · ±' + gps.accuracy + 'm';
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<div class="sys-location-source">Source: ' + escHtml(locationData.source || 'unknown') + '</div>';
|
||||
}
|
||||
} else {
|
||||
html += '<div class="sys-location-coords" style="color:var(--text-dim)">No location</div>';
|
||||
}
|
||||
|
||||
@@ -3151,10 +3151,14 @@
|
||||
<div class="sys-card-header">Network</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Connecting…</span></div>
|
||||
</div>
|
||||
<div class="sys-card sys-card-wide" id="sysCardLocation">
|
||||
<div class="sys-card" id="sysCardLocation">
|
||||
<div class="sys-card-header">Location & Weather</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Loading…</span></div>
|
||||
</div>
|
||||
<div class="sys-card" id="sysCardInfo">
|
||||
<div class="sys-card-header">System Info</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Connecting…</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: EQUIPMENT & OPERATIONS -->
|
||||
<div class="sys-group-header">Equipment & Operations</div>
|
||||
@@ -3170,12 +3174,6 @@
|
||||
<div class="sys-card-header">Active Processes</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Connecting…</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Full-width system info -->
|
||||
<div class="sys-card sys-card-full" id="sysCardInfo">
|
||||
<div class="sys-card-header">System Info</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Connecting…</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -123,8 +123,7 @@ def test_stream_returns_sse_content_type(client):
|
||||
def test_location_returns_shape(client):
|
||||
"""GET /system/location returns lat/lon/source shape."""
|
||||
_login(client)
|
||||
with patch('routes.system.contextlib.suppress'):
|
||||
resp = client.get('/system/location')
|
||||
resp = client.get('/system/location')
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert 'lat' in data
|
||||
@@ -132,12 +131,48 @@ def test_location_returns_shape(client):
|
||||
assert 'source' in data
|
||||
|
||||
|
||||
def test_location_from_gps(client):
|
||||
"""Location endpoint returns GPS data when fix available."""
|
||||
_login(client)
|
||||
mock_pos = MagicMock()
|
||||
mock_pos.fix_quality = 3
|
||||
mock_pos.latitude = 51.5074
|
||||
mock_pos.longitude = -0.1278
|
||||
mock_pos.satellites = 12
|
||||
mock_pos.epx = 2.5
|
||||
mock_pos.epy = 3.1
|
||||
mock_pos.altitude = 45.0
|
||||
|
||||
with patch('routes.system.get_current_position', return_value=mock_pos, create=True):
|
||||
# Patch the import inside the function
|
||||
import routes.system as mod
|
||||
original = mod._get_observer_location
|
||||
|
||||
def _patched():
|
||||
with patch('utils.gps.get_current_position', return_value=mock_pos):
|
||||
return original()
|
||||
|
||||
mod._get_observer_location = _patched
|
||||
try:
|
||||
resp = client.get('/system/location')
|
||||
finally:
|
||||
mod._get_observer_location = original
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data['source'] == 'gps'
|
||||
assert data['lat'] == 51.5074
|
||||
assert data['lon'] == -0.1278
|
||||
assert data['gps']['fix_quality'] == 3
|
||||
assert data['gps']['satellites'] == 12
|
||||
assert data['gps']['accuracy'] == 3.1
|
||||
assert data['gps']['altitude'] == 45.0
|
||||
|
||||
|
||||
def test_location_falls_back_to_config(client):
|
||||
"""Location endpoint returns config defaults when GPS unavailable."""
|
||||
_login(client)
|
||||
with patch('routes.system.DEFAULT_LATITUDE', 40.7128, create=True), \
|
||||
patch('routes.system.DEFAULT_LONGITUDE', -74.006, create=True):
|
||||
# Mock the import inside _get_observer_location
|
||||
with patch('utils.gps.get_current_position', return_value=None):
|
||||
resp = client.get('/system/location')
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
|
||||
Reference in New Issue
Block a user