feat: Add location settings for ISS pass predictions

- Add Location tab to settings modal with lat/lon inputs
- Add GPS detection button for auto-location
- Update SSTV to use saved location for ISS pass predictions
- Fix SSTV panels to use full screen width (remove max-width constraint)
- Improve ISS pass messages to guide users to location settings
- Add checked/last_check fields to update status response

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-29 15:36:37 +00:00
parent 8e204725b2
commit 0c7ac816e9
5 changed files with 226 additions and 14 deletions

View File

@@ -164,8 +164,7 @@
display: flex;
flex-direction: column;
flex: 1;
min-width: 340px;
max-width: 400px;
min-width: 300px;
}
.sstv-live-header {
@@ -290,8 +289,8 @@
overflow: hidden;
display: flex;
flex-direction: column;
flex: 2;
min-width: 0;
flex: 1.5;
min-width: 300px;
}
.sstv-gallery-header {

View File

@@ -540,6 +540,133 @@ document.addEventListener('DOMContentLoaded', () => {
Settings.init();
});
// =============================================================================
// Location Settings Functions
// =============================================================================
/**
* Load and display current observer location
*/
function loadObserverLocation() {
const lat = localStorage.getItem('observerLat');
const lon = localStorage.getItem('observerLon');
const latInput = document.getElementById('observerLatInput');
const lonInput = document.getElementById('observerLonInput');
const currentLatDisplay = document.getElementById('currentLatDisplay');
const currentLonDisplay = document.getElementById('currentLonDisplay');
if (latInput && lat) latInput.value = lat;
if (lonInput && lon) lonInput.value = lon;
if (currentLatDisplay) {
currentLatDisplay.textContent = lat ? parseFloat(lat).toFixed(4) + '°' : 'Not set';
}
if (currentLonDisplay) {
currentLonDisplay.textContent = lon ? parseFloat(lon).toFixed(4) + '°' : 'Not set';
}
}
/**
* Detect location using browser GPS
*/
function detectLocationGPS() {
const latInput = document.getElementById('observerLatInput');
const lonInput = document.getElementById('observerLonInput');
if (!navigator.geolocation) {
if (typeof showNotification === 'function') {
showNotification('Location', 'GPS not available in this browser');
} else {
alert('GPS not available in this browser');
}
return;
}
// Show loading state
const btn = event.target.closest('button');
const originalText = btn.innerHTML;
btn.innerHTML = '<span style="opacity: 0.7;">Detecting...</span>';
btn.disabled = true;
navigator.geolocation.getCurrentPosition(
(pos) => {
if (latInput) latInput.value = pos.coords.latitude.toFixed(4);
if (lonInput) lonInput.value = pos.coords.longitude.toFixed(4);
btn.innerHTML = originalText;
btn.disabled = false;
if (typeof showNotification === 'function') {
showNotification('Location', 'GPS coordinates detected');
}
},
(err) => {
btn.innerHTML = originalText;
btn.disabled = false;
let message = 'Failed to get location';
if (err.code === 1) message = 'Location access denied';
else if (err.code === 2) message = 'Location unavailable';
else if (err.code === 3) message = 'Location request timed out';
if (typeof showNotification === 'function') {
showNotification('Location', message);
} else {
alert(message);
}
},
{ enableHighAccuracy: true, timeout: 10000 }
);
}
/**
* Save observer location to localStorage
*/
function saveObserverLocation() {
const latInput = document.getElementById('observerLatInput');
const lonInput = document.getElementById('observerLonInput');
const lat = parseFloat(latInput?.value);
const lon = parseFloat(lonInput?.value);
if (isNaN(lat) || lat < -90 || lat > 90) {
if (typeof showNotification === 'function') {
showNotification('Location', 'Invalid latitude (must be -90 to 90)');
} else {
alert('Invalid latitude (must be -90 to 90)');
}
return;
}
if (isNaN(lon) || lon < -180 || lon > 180) {
if (typeof showNotification === 'function') {
showNotification('Location', 'Invalid longitude (must be -180 to 180)');
} else {
alert('Invalid longitude (must be -180 to 180)');
}
return;
}
localStorage.setItem('observerLat', lat.toString());
localStorage.setItem('observerLon', lon.toString());
// Update display
const currentLatDisplay = document.getElementById('currentLatDisplay');
const currentLonDisplay = document.getElementById('currentLonDisplay');
if (currentLatDisplay) currentLatDisplay.textContent = lat.toFixed(4) + '°';
if (currentLonDisplay) currentLonDisplay.textContent = lon.toFixed(4) + '°';
if (typeof showNotification === 'function') {
showNotification('Location', 'Observer location saved');
}
// Refresh SSTV ISS schedule if available
if (typeof SSTV !== 'undefined' && typeof SSTV.loadIssSchedule === 'function') {
SSTV.loadIssSchedule();
}
}
// =============================================================================
// Update Settings Functions
// =============================================================================
@@ -709,5 +836,7 @@ function switchSettingsTab(tabName) {
loadSettingsTools();
} else if (tabName === 'updates') {
loadUpdateStatus();
} else if (tabName === 'location') {
loadObserverLocation();
}
}

View File

@@ -312,33 +312,45 @@ const SSTV = (function() {
* Load ISS pass schedule
*/
async function loadIssSchedule() {
// Try to get user's location
const lat = localStorage.getItem('observerLat') || 51.5074;
const lon = localStorage.getItem('observerLon') || -0.1278;
// Try to get user's location from settings
const storedLat = localStorage.getItem('observerLat');
const storedLon = localStorage.getItem('observerLon');
// Check if location is actually set
const hasLocation = storedLat !== null && storedLon !== null;
const lat = storedLat || 51.5074;
const lon = storedLon || -0.1278;
try {
const response = await fetch(`/sstv/iss-schedule?latitude=${lat}&longitude=${lon}&hours=48`);
const data = await response.json();
if (data.status === 'ok' && data.passes && data.passes.length > 0) {
renderIssInfo(data.passes[0]);
renderIssInfo(data.passes[0], hasLocation);
} else {
renderIssInfo(null);
renderIssInfo(null, hasLocation);
}
} catch (err) {
console.error('Failed to load ISS schedule:', err);
renderIssInfo(null);
renderIssInfo(null, hasLocation);
}
}
/**
* Render ISS pass info
*/
function renderIssInfo(nextPass) {
function renderIssInfo(nextPass, hasLocation = true) {
const container = document.getElementById('sstvIssInfo');
if (!container) return;
if (!nextPass) {
const locationMsg = hasLocation
? 'No passes in next 48 hours'
: 'Set location in Settings > Location tab';
const noteMsg = hasLocation
? 'Check ARISS.org for SSTV event schedules'
: 'Click the gear icon to open Settings';
container.innerHTML = `
<div class="sstv-iss-info">
<svg class="sstv-iss-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -348,8 +360,8 @@ const SSTV = (function() {
</svg>
<div class="sstv-iss-details">
<div class="sstv-iss-label">Next ISS Pass</div>
<div class="sstv-iss-value">Unknown - Set location in settings</div>
<div class="sstv-iss-note">Check ARISS.org for SSTV event schedules</div>
<div class="sstv-iss-value">${locationMsg}</div>
<div class="sstv-iss-note">${noteMsg}</div>
</div>
</div>
`;
@@ -443,6 +455,7 @@ const SSTV = (function() {
start,
stop,
loadImages,
loadIssSchedule,
showImage,
closeImage
};

View File

@@ -11,6 +11,7 @@
<div class="settings-tabs">
<button class="settings-tab active" data-tab="offline" onclick="switchSettingsTab('offline')">Offline</button>
<button class="settings-tab" data-tab="location" onclick="switchSettingsTab('location')">Location</button>
<button class="settings-tab" data-tab="display" onclick="switchSettingsTab('display')">Display</button>
<button class="settings-tab" data-tab="updates" onclick="switchSettingsTab('updates')">Updates</button>
<button class="settings-tab" data-tab="tools" onclick="switchSettingsTab('tools')">Tools</button>
@@ -119,6 +120,72 @@
</div>
</div>
<!-- Location Section -->
<div id="settings-location" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Observer Location</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
Set your geographic coordinates for satellite pass predictions and ISS tracking.
</p>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Latitude</span>
<span class="settings-label-desc">Decimal degrees (-90 to 90)</span>
</div>
<input type="number" id="observerLatInput" class="settings-input"
step="0.0001" min="-90" max="90" placeholder="51.5074"
style="width: 120px; text-align: right;">
</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Longitude</span>
<span class="settings-label-desc">Decimal degrees (-180 to 180)</span>
</div>
<input type="number" id="observerLonInput" class="settings-input"
step="0.0001" min="-180" max="180" placeholder="-0.1278"
style="width: 120px; text-align: right;">
</div>
<div style="display: flex; gap: 10px; margin-top: 15px;">
<button class="check-assets-btn" onclick="detectLocationGPS()" style="flex: 1;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px;">
<circle cx="12" cy="12" r="10"/>
<circle cx="12" cy="12" r="3"/>
<line x1="12" y1="2" x2="12" y2="6"/>
<line x1="12" y1="18" x2="12" y2="22"/>
<line x1="2" y1="12" x2="6" y2="12"/>
<line x1="18" y1="12" x2="22" y2="12"/>
</svg>
Use GPS
</button>
<button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;">
Save Location
</button>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Current Location</div>
<div id="currentLocationDisplay" style="padding: 12px; background: var(--bg-tertiary); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="color: var(--text-dim);">Latitude</span>
<span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-dim);">Longitude</span>
<span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span>
</div>
</div>
</div>
<div class="settings-info">
<strong>Note:</strong> Location is used for ISS pass predictions in SSTV mode and satellite tracking.
Your location is stored locally and never sent to external servers.
</div>
</div>
<!-- Display Section -->
<div id="settings-display" class="settings-section">
<div class="settings-group">

View File

@@ -174,6 +174,7 @@ def check_for_updates(force: bool = False) -> dict[str, Any]:
return {
'success': True,
'checked': True,
'update_available': update_available,
'show_notification': show_notification,
'current_version': current_version,
@@ -196,6 +197,7 @@ def check_for_updates(force: bool = False) -> dict[str, Any]:
update_available = _compare_versions(current_version, cached_version) < 0
return {
'success': True,
'checked': True,
'update_available': update_available,
'current_version': current_version,
'latest_version': cached_version,
@@ -223,6 +225,7 @@ def check_for_updates(force: bool = False) -> dict[str, Any]:
return {
'success': True,
'checked': True,
'update_available': update_available,
'show_notification': show_notification,
'current_version': current_version,
@@ -231,7 +234,8 @@ def check_for_updates(force: bool = False) -> dict[str, Any]:
'release_notes': release['body'] or '',
'release_name': release['name'] or f'v{latest_version}',
'published_at': release['published_at'],
'cached': False
'cached': False,
'last_check': datetime.now().isoformat()
}