mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
fix: Persist tracked satellites to database (fixes #135)
Satellites added via CelesTrak import or TLE paste are now stored in SQLite and survive page reloads and app restarts. Adds CRUD API endpoints and wires frontend sidebar + dashboard to use them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,13 @@ from flask import Blueprint, jsonify, request, render_template, Response
|
||||
from config import SHARED_OBSERVER_LOCATION_ENABLED
|
||||
|
||||
from data.satellites import TLE_SATELLITES
|
||||
from utils.database import (
|
||||
get_tracked_satellites,
|
||||
add_tracked_satellite,
|
||||
bulk_add_tracked_satellites,
|
||||
update_tracked_satellite,
|
||||
remove_tracked_satellite,
|
||||
)
|
||||
from utils.logging import satellite_logger as logger
|
||||
from utils.validation import validate_latitude, validate_longitude, validate_hours, validate_elevation
|
||||
|
||||
@@ -31,18 +38,38 @@ ALLOWED_TLE_HOSTS = ['celestrak.org', 'celestrak.com', 'www.celestrak.org', 'www
|
||||
_tle_cache = dict(TLE_SATELLITES)
|
||||
|
||||
|
||||
def _load_db_satellites_into_cache():
|
||||
"""Load user-tracked satellites from DB into the TLE cache."""
|
||||
global _tle_cache
|
||||
try:
|
||||
db_sats = get_tracked_satellites()
|
||||
loaded = 0
|
||||
for sat in db_sats:
|
||||
if sat['tle_line1'] and sat['tle_line2']:
|
||||
# Use a cache key derived from name (sanitised)
|
||||
cache_key = sat['name'].replace(' ', '-').upper()
|
||||
if cache_key not in _tle_cache:
|
||||
_tle_cache[cache_key] = (sat['name'], sat['tle_line1'], sat['tle_line2'])
|
||||
loaded += 1
|
||||
if loaded:
|
||||
logger.info(f"Loaded {loaded} user-tracked satellites into TLE cache")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load DB satellites into TLE cache: {e}")
|
||||
|
||||
|
||||
def init_tle_auto_refresh():
|
||||
"""Initialize TLE auto-refresh. Called by app.py after initialization."""
|
||||
import threading
|
||||
|
||||
|
||||
def _auto_refresh_tle():
|
||||
try:
|
||||
_load_db_satellites_into_cache()
|
||||
updated = refresh_tle_data()
|
||||
if updated:
|
||||
logger.info(f"Auto-refreshed TLE data for: {', '.join(updated)}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Auto TLE refresh failed: {e}")
|
||||
|
||||
|
||||
# Start auto-refresh in background
|
||||
threading.Timer(2.0, _auto_refresh_tle).start()
|
||||
logger.info("TLE auto-refresh scheduled")
|
||||
@@ -168,13 +195,13 @@ def predict_passes():
|
||||
except ValueError as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
norad_to_name = {
|
||||
25544: 'ISS',
|
||||
40069: 'METEOR-M2',
|
||||
57166: 'METEOR-M2-3'
|
||||
}
|
||||
|
||||
sat_input = data.get('satellites', ['ISS', 'METEOR-M2', 'METEOR-M2-3'])
|
||||
norad_to_name = {
|
||||
25544: 'ISS',
|
||||
40069: 'METEOR-M2',
|
||||
57166: 'METEOR-M2-3'
|
||||
}
|
||||
|
||||
sat_input = data.get('satellites', ['ISS', 'METEOR-M2', 'METEOR-M2-3'])
|
||||
satellites = []
|
||||
for sat in sat_input:
|
||||
if isinstance(sat, int) and sat in norad_to_name:
|
||||
@@ -183,11 +210,11 @@ def predict_passes():
|
||||
satellites.append(sat)
|
||||
|
||||
passes = []
|
||||
colors = {
|
||||
'ISS': '#00ffff',
|
||||
'METEOR-M2': '#9370DB',
|
||||
'METEOR-M2-3': '#ff00ff'
|
||||
}
|
||||
colors = {
|
||||
'ISS': '#00ffff',
|
||||
'METEOR-M2': '#9370DB',
|
||||
'METEOR-M2-3': '#ff00ff'
|
||||
}
|
||||
name_to_norad = {v: k for k, v in norad_to_name.items()}
|
||||
|
||||
ts = load.timescale()
|
||||
@@ -319,11 +346,11 @@ def get_satellite_position():
|
||||
sat_input = data.get('satellites', [])
|
||||
include_track = bool(data.get('includeTrack', True))
|
||||
|
||||
norad_to_name = {
|
||||
25544: 'ISS',
|
||||
40069: 'METEOR-M2',
|
||||
57166: 'METEOR-M2-3'
|
||||
}
|
||||
norad_to_name = {
|
||||
25544: 'ISS',
|
||||
40069: 'METEOR-M2',
|
||||
57166: 'METEOR-M2-3'
|
||||
}
|
||||
|
||||
satellites = []
|
||||
for sat in sat_input:
|
||||
@@ -543,3 +570,75 @@ def fetch_celestrak(category):
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching CelesTrak data: {e}")
|
||||
return jsonify({'status': 'error', 'message': 'Failed to fetch satellite data'})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tracked Satellites CRUD
|
||||
# =============================================================================
|
||||
|
||||
@satellite_bp.route('/tracked', methods=['GET'])
|
||||
def list_tracked_satellites():
|
||||
"""Return all tracked satellites from the database."""
|
||||
enabled_only = request.args.get('enabled', '').lower() == 'true'
|
||||
sats = get_tracked_satellites(enabled_only=enabled_only)
|
||||
return jsonify({'status': 'success', 'satellites': sats})
|
||||
|
||||
|
||||
@satellite_bp.route('/tracked', methods=['POST'])
|
||||
def add_tracked_satellites_endpoint():
|
||||
"""Add one or more tracked satellites."""
|
||||
global _tle_cache
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({'status': 'error', 'message': 'No data provided'}), 400
|
||||
|
||||
# Accept a single satellite dict or a list
|
||||
sat_list = data if isinstance(data, list) else [data]
|
||||
|
||||
added = 0
|
||||
for sat in sat_list:
|
||||
norad_id = str(sat.get('norad_id', sat.get('norad', '')))
|
||||
name = sat.get('name', '')
|
||||
if not norad_id or not name:
|
||||
continue
|
||||
tle1 = sat.get('tle_line1', sat.get('tle1'))
|
||||
tle2 = sat.get('tle_line2', sat.get('tle2'))
|
||||
enabled = sat.get('enabled', True)
|
||||
|
||||
if add_tracked_satellite(norad_id, name, tle1, tle2, enabled):
|
||||
added += 1
|
||||
|
||||
# Also inject into TLE cache if we have TLE data
|
||||
if tle1 and tle2:
|
||||
cache_key = name.replace(' ', '-').upper()
|
||||
_tle_cache[cache_key] = (name, tle1, tle2)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'added': added,
|
||||
'satellites': get_tracked_satellites(),
|
||||
})
|
||||
|
||||
|
||||
@satellite_bp.route('/tracked/<norad_id>', methods=['PUT'])
|
||||
def update_tracked_satellite_endpoint(norad_id):
|
||||
"""Update the enabled state of a tracked satellite."""
|
||||
data = request.json or {}
|
||||
enabled = data.get('enabled')
|
||||
if enabled is None:
|
||||
return jsonify({'status': 'error', 'message': 'Missing enabled field'}), 400
|
||||
|
||||
ok = update_tracked_satellite(str(norad_id), bool(enabled))
|
||||
if ok:
|
||||
return jsonify({'status': 'success'})
|
||||
return jsonify({'status': 'error', 'message': 'Satellite not found'}), 404
|
||||
|
||||
|
||||
@satellite_bp.route('/tracked/<norad_id>', methods=['DELETE'])
|
||||
def delete_tracked_satellite_endpoint(norad_id):
|
||||
"""Remove a tracked satellite by NORAD ID."""
|
||||
ok, msg = remove_tracked_satellite(str(norad_id))
|
||||
if ok:
|
||||
return jsonify({'status': 'success', 'message': msg})
|
||||
status_code = 403 if 'builtin' in msg.lower() else 404
|
||||
return jsonify({'status': 'error', 'message': msg}), status_code
|
||||
|
||||
@@ -10621,10 +10621,7 @@
|
||||
}
|
||||
|
||||
// Satellite management
|
||||
let trackedSatellites = [
|
||||
{ id: 'ISS', name: 'ISS (ZARYA)', norad: '25544', builtin: true, checked: true },
|
||||
{ id: 'METEOR-M2', name: 'Meteor-M 2', norad: '40069', builtin: true, checked: true }
|
||||
];
|
||||
let trackedSatellites = [];
|
||||
|
||||
function renderSatelliteList() {
|
||||
const list = document.getElementById('satTrackingList');
|
||||
@@ -10643,14 +10640,27 @@
|
||||
}
|
||||
|
||||
function toggleSatellite(idx) {
|
||||
trackedSatellites[idx].checked = !trackedSatellites[idx].checked;
|
||||
const sat = trackedSatellites[idx];
|
||||
sat.checked = !sat.checked;
|
||||
fetch(`/satellite/tracked/${sat.norad}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: sat.checked })
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function removeSatellite(idx) {
|
||||
if (!trackedSatellites[idx].builtin) {
|
||||
trackedSatellites.splice(idx, 1);
|
||||
renderSatelliteList();
|
||||
}
|
||||
const sat = trackedSatellites[idx];
|
||||
if (sat.builtin) return;
|
||||
fetch(`/satellite/tracked/${sat.norad}`, { method: 'DELETE' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
trackedSatellites.splice(idx, 1);
|
||||
renderSatelliteList();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function getSelectedSatellites() {
|
||||
@@ -10686,7 +10696,7 @@
|
||||
}
|
||||
|
||||
const lines = tleText.split('\\n').map(l => l.trim()).filter(l => l);
|
||||
let added = 0;
|
||||
const toAdd = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i += 3) {
|
||||
if (i + 2 < lines.length) {
|
||||
@@ -10696,32 +10706,33 @@
|
||||
|
||||
if (line1.startsWith('1 ') && line2.startsWith('2 ')) {
|
||||
const norad = line1.substring(2, 7).trim();
|
||||
const id = name.replace(/[^a-zA-Z0-9]/g, '-').toUpperCase();
|
||||
|
||||
// Check if already exists
|
||||
if (!trackedSatellites.find(s => s.norad === norad)) {
|
||||
trackedSatellites.push({
|
||||
id: id,
|
||||
name: name,
|
||||
norad: norad,
|
||||
builtin: false,
|
||||
checked: true,
|
||||
tle: [name, line1, line2]
|
||||
});
|
||||
added++;
|
||||
toAdd.push({ norad_id: norad, name: name, tle1: line1, tle2: line2, enabled: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0) {
|
||||
renderSatelliteList();
|
||||
document.getElementById('tleInput').value = '';
|
||||
closeSatModal();
|
||||
showInfo(`Added ${added} satellite(s)`);
|
||||
} else {
|
||||
if (toAdd.length === 0) {
|
||||
alert('No valid TLE data found. Format: Name, Line 1, Line 2 (3 lines per satellite)');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/satellite/tracked', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(toAdd)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
_loadSatellitesFromAPI();
|
||||
document.getElementById('tleInput').value = '';
|
||||
closeSatModal();
|
||||
showInfo(`Added ${data.added} satellite(s)`);
|
||||
}
|
||||
})
|
||||
.catch(() => alert('Failed to save satellites'));
|
||||
}
|
||||
|
||||
function fetchCelestrak() {
|
||||
@@ -10737,35 +10748,76 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.satellites) {
|
||||
let added = 0;
|
||||
data.satellites.forEach(sat => {
|
||||
const noradStr = String(sat.norad);
|
||||
if (!trackedSatellites.find(s => s.norad === noradStr)) {
|
||||
trackedSatellites.push({
|
||||
id: sat.name.replace(/[^a-zA-Z0-9-]/g, '-'),
|
||||
name: sat.name,
|
||||
norad: noradStr,
|
||||
builtin: false,
|
||||
checked: false, // Don't auto-select
|
||||
tle: [sat.name, sat.tle1, sat.tle2]
|
||||
});
|
||||
added++;
|
||||
const toAdd = data.satellites
|
||||
.filter(sat => !trackedSatellites.find(s => s.norad === String(sat.norad)))
|
||||
.map(sat => ({
|
||||
norad_id: String(sat.norad),
|
||||
name: sat.name,
|
||||
tle1: sat.tle1,
|
||||
tle2: sat.tle2,
|
||||
enabled: false
|
||||
}));
|
||||
|
||||
if (toAdd.length === 0) {
|
||||
status.innerHTML = `<span style="color: var(--accent-green);">All ${data.satellites.length} satellites already tracked</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/satellite/tracked', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(toAdd)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.status === 'success') {
|
||||
_loadSatellitesFromAPI();
|
||||
status.innerHTML = `<span style="color: var(--accent-green);">Added ${result.added} satellites (${data.satellites.length} total in category)</span>`;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
status.innerHTML = `<span style="color: var(--accent-red);">Failed to save satellites</span>`;
|
||||
});
|
||||
renderSatelliteList();
|
||||
status.innerHTML = `<span style="color: var(--accent-green);">Added ${added} satellites (${data.satellites.length} total in category)</span>`;
|
||||
} else {
|
||||
status.innerHTML = `<span style="color: var(--accent-red);">Error: ${data.message || 'Failed to fetch'}</span>`;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(() => {
|
||||
status.innerHTML = `<span style="color: var(--accent-red);">Network error</span>`;
|
||||
});
|
||||
}
|
||||
|
||||
function _loadSatellitesFromAPI() {
|
||||
fetch('/satellite/tracked')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.satellites) {
|
||||
trackedSatellites = data.satellites.map(sat => ({
|
||||
id: sat.name.replace(/[^a-zA-Z0-9]/g, '-').toUpperCase(),
|
||||
name: sat.name,
|
||||
norad: sat.norad_id,
|
||||
builtin: sat.builtin,
|
||||
checked: sat.enabled,
|
||||
tle: sat.tle_line1 ? [sat.name, sat.tle_line1, sat.tle_line2] : null
|
||||
}));
|
||||
renderSatelliteList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Fallback to hardcoded defaults if API fails
|
||||
if (trackedSatellites.length === 0) {
|
||||
trackedSatellites = [
|
||||
{ id: 'ISS', name: 'ISS (ZARYA)', norad: '25544', builtin: true, checked: true },
|
||||
{ id: 'METEOR-M2', name: 'Meteor-M 2', norad: '40069', builtin: true, checked: true }
|
||||
];
|
||||
renderSatelliteList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize satellite list when satellite mode is loaded
|
||||
function initSatelliteList() {
|
||||
renderSatelliteList();
|
||||
_loadSatellitesFromAPI();
|
||||
}
|
||||
|
||||
// Utility function
|
||||
|
||||
@@ -269,12 +269,42 @@
|
||||
let currentLocationSource = 'local';
|
||||
let agents = [];
|
||||
|
||||
const satellites = {
|
||||
let satellites = {
|
||||
25544: { name: 'ISS (ZARYA)', color: '#00ffff' },
|
||||
40069: { name: 'METEOR-M2', color: '#9370DB' },
|
||||
57166: { name: 'METEOR-M2-3', color: '#ff00ff' }
|
||||
};
|
||||
|
||||
const satColors = ['#00ffff', '#9370DB', '#ff00ff', '#00ff00', '#ff6600', '#ffff00', '#ff69b4', '#7b68ee'];
|
||||
|
||||
function loadDashboardSatellites() {
|
||||
fetch('/satellite/tracked?enabled=true')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.satellites && data.satellites.length > 0) {
|
||||
const newSats = {};
|
||||
const select = document.getElementById('satSelect');
|
||||
select.innerHTML = '';
|
||||
data.satellites.forEach((sat, i) => {
|
||||
const norad = parseInt(sat.norad_id);
|
||||
newSats[norad] = {
|
||||
name: sat.name,
|
||||
color: satellites[norad]?.color || satColors[i % satColors.length]
|
||||
};
|
||||
const opt = document.createElement('option');
|
||||
opt.value = norad;
|
||||
opt.textContent = sat.name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
satellites = newSats;
|
||||
// Default to ISS if available
|
||||
if (newSats[25544]) select.value = '25544';
|
||||
selectedSatellite = parseInt(select.value);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function onSatelliteChange() {
|
||||
const select = document.getElementById('satSelect');
|
||||
selectedSatellite = parseInt(select.value);
|
||||
@@ -331,6 +361,7 @@
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDashboardSatellites();
|
||||
setupEmbeddedMode();
|
||||
const usedShared = applySharedObserverLocation();
|
||||
initGroundMap();
|
||||
|
||||
@@ -531,6 +531,30 @@ def init_db() -> None:
|
||||
ON push_payloads(agent_id, received_at)
|
||||
''')
|
||||
|
||||
# Tracked satellites table for persistent satellite management
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS tracked_satellites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
norad_id TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
tle_line1 TEXT,
|
||||
tle_line2 TEXT,
|
||||
enabled BOOLEAN DEFAULT 1,
|
||||
builtin BOOLEAN DEFAULT 0,
|
||||
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# Seed builtin satellites if not already present
|
||||
conn.execute('''
|
||||
INSERT OR IGNORE INTO tracked_satellites (norad_id, name, tle_line1, tle_line2, enabled, builtin)
|
||||
VALUES ('25544', 'ISS (ZARYA)', NULL, NULL, 1, 1)
|
||||
''')
|
||||
conn.execute('''
|
||||
INSERT OR IGNORE INTO tracked_satellites (norad_id, name, tle_line1, tle_line2, enabled, builtin)
|
||||
VALUES ('40069', 'METEOR-M2', NULL, NULL, 1, 1)
|
||||
''')
|
||||
|
||||
logger.info("Database initialized successfully")
|
||||
|
||||
|
||||
@@ -2170,3 +2194,111 @@ def cleanup_old_payloads(max_age_hours: int = 24) -> int:
|
||||
''', (f'-{max_age_hours} hours',))
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tracked Satellites Functions
|
||||
# =============================================================================
|
||||
|
||||
def get_tracked_satellites(enabled_only: bool = False) -> list[dict]:
|
||||
"""Return all tracked satellites, optionally filtered to enabled only."""
|
||||
with get_db() as conn:
|
||||
if enabled_only:
|
||||
rows = conn.execute(
|
||||
'SELECT norad_id, name, tle_line1, tle_line2, enabled, builtin, added_at '
|
||||
'FROM tracked_satellites WHERE enabled = 1 ORDER BY builtin DESC, name'
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
'SELECT norad_id, name, tle_line1, tle_line2, enabled, builtin, added_at '
|
||||
'FROM tracked_satellites ORDER BY builtin DESC, name'
|
||||
).fetchall()
|
||||
return [
|
||||
{
|
||||
'norad_id': r[0],
|
||||
'name': r[1],
|
||||
'tle_line1': r[2],
|
||||
'tle_line2': r[3],
|
||||
'enabled': bool(r[4]),
|
||||
'builtin': bool(r[5]),
|
||||
'added_at': r[6],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def add_tracked_satellite(
|
||||
norad_id: str,
|
||||
name: str,
|
||||
tle_line1: str | None = None,
|
||||
tle_line2: str | None = None,
|
||||
enabled: bool = True,
|
||||
builtin: bool = False,
|
||||
) -> bool:
|
||||
"""Insert a tracked satellite. Returns True if inserted, False if duplicate."""
|
||||
with get_db() as conn:
|
||||
try:
|
||||
conn.execute(
|
||||
'INSERT OR IGNORE INTO tracked_satellites '
|
||||
'(norad_id, name, tle_line1, tle_line2, enabled, builtin) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?)',
|
||||
(str(norad_id), name, tle_line1, tle_line2, int(enabled), int(builtin)),
|
||||
)
|
||||
return conn.total_changes > 0
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Error adding tracked satellite {norad_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def bulk_add_tracked_satellites(satellites_list: list[dict]) -> int:
|
||||
"""Insert many tracked satellites at once. Returns count of newly inserted."""
|
||||
added = 0
|
||||
with get_db() as conn:
|
||||
for sat in satellites_list:
|
||||
try:
|
||||
cursor = conn.execute(
|
||||
'INSERT OR IGNORE INTO tracked_satellites '
|
||||
'(norad_id, name, tle_line1, tle_line2, enabled, builtin) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?)',
|
||||
(
|
||||
str(sat['norad_id']),
|
||||
sat['name'],
|
||||
sat.get('tle_line1'),
|
||||
sat.get('tle_line2'),
|
||||
int(sat.get('enabled', True)),
|
||||
int(sat.get('builtin', False)),
|
||||
),
|
||||
)
|
||||
if cursor.rowcount > 0:
|
||||
added += 1
|
||||
except (sqlite3.Error, KeyError) as e:
|
||||
logger.warning(f"Error bulk-adding satellite: {e}")
|
||||
return added
|
||||
|
||||
|
||||
def update_tracked_satellite(norad_id: str, enabled: bool) -> bool:
|
||||
"""Toggle enabled state for a tracked satellite."""
|
||||
with get_db() as conn:
|
||||
cursor = conn.execute(
|
||||
'UPDATE tracked_satellites SET enabled = ? WHERE norad_id = ?',
|
||||
(int(enabled), str(norad_id)),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def remove_tracked_satellite(norad_id: str) -> tuple[bool, str]:
|
||||
"""Delete a tracked satellite by NORAD ID. Refuses to delete builtins."""
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
'SELECT builtin FROM tracked_satellites WHERE norad_id = ?',
|
||||
(str(norad_id),),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return False, 'Satellite not found'
|
||||
if row[0]:
|
||||
return False, 'Cannot remove builtin satellite'
|
||||
conn.execute(
|
||||
'DELETE FROM tracked_satellites WHERE norad_id = ?',
|
||||
(str(norad_id),),
|
||||
)
|
||||
return True, 'Removed'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user