Files
intercept/routes/offline.py
Smittix 90281b1535 fix(modes): deep-linked mode scripts fail when body not yet parsed
ensureModeScript() used document.body.appendChild() to load lazy mode
scripts, but the preload for ?mode= query params runs in <head> before
<body> exists, causing all deep-linked modes to silently fail.

Also fix cross-mode handoffs (BT→BT Locate, WiFi→WiFi Locate,
Spy Stations→Waterfall) that assumed target module was already loaded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 20:49:08 +00:00

154 lines
4.8 KiB
Python

"""
Offline mode routes - Asset management and settings for offline operation.
"""
from flask import Blueprint, jsonify, request
from utils.database import get_setting, set_setting
from utils.responses import api_success, api_error
import os
offline_bp = Blueprint('offline', __name__, url_prefix='/offline')
# Default offline settings
OFFLINE_DEFAULTS = {
'offline.enabled': False,
# Default to bundled assets/fonts to avoid third-party CDN privacy blocks.
'offline.assets_source': 'local',
'offline.fonts_source': 'local',
'offline.tile_provider': 'cartodb_dark_cyan',
'offline.tile_server_url': ''
}
# Asset paths to check
ASSET_PATHS = {
'leaflet': [
'static/vendor/leaflet/leaflet.js',
'static/vendor/leaflet/leaflet.css'
],
'chartjs': [
'static/vendor/chartjs/chart.umd.min.js'
],
'inter': [
'static/vendor/fonts/Inter-Regular.woff2',
'static/vendor/fonts/Inter-Medium.woff2',
'static/vendor/fonts/Inter-SemiBold.woff2',
'static/vendor/fonts/Inter-Bold.woff2'
],
'jetbrains': [
'static/vendor/fonts/JetBrainsMono-Regular.woff2',
'static/vendor/fonts/JetBrainsMono-Medium.woff2',
'static/vendor/fonts/JetBrainsMono-SemiBold.woff2',
'static/vendor/fonts/JetBrainsMono-Bold.woff2'
],
'leaflet_images': [
'static/vendor/leaflet/images/marker-icon.png',
'static/vendor/leaflet/images/marker-icon-2x.png',
'static/vendor/leaflet/images/marker-shadow.png',
'static/vendor/leaflet/images/layers.png',
'static/vendor/leaflet/images/layers-2x.png'
],
'leaflet_heat': [
'static/vendor/leaflet-heat/leaflet-heat.js'
]
}
def get_offline_settings():
"""Get all offline settings with defaults."""
settings = {}
for key, default in OFFLINE_DEFAULTS.items():
settings[key] = get_setting(key, default)
return settings
@offline_bp.route('/settings', methods=['GET'])
def get_settings():
"""Get current offline settings."""
settings = get_offline_settings()
return api_success(data={'settings': settings})
@offline_bp.route('/settings', methods=['POST'])
def save_setting():
"""Save an offline setting."""
data = request.get_json()
if not data or 'key' not in data or 'value' not in data:
return api_error('Missing key or value', 400)
key = data['key']
value = data['value']
# Validate key is an allowed setting
if key not in OFFLINE_DEFAULTS:
return api_error(f'Unknown setting: {key}', 400)
# Validate value type matches default
default_type = type(OFFLINE_DEFAULTS[key])
if not isinstance(value, default_type):
# Try to convert
try:
if default_type == bool:
value = str(value).lower() in ('true', '1', 'yes')
else:
value = default_type(value)
except (ValueError, TypeError):
return api_error(f'Invalid value type for {key}', 400)
set_setting(key, value)
return api_success(data={'key': key, 'value': value})
@offline_bp.route('/status', methods=['GET'])
def get_status():
"""Check status of local assets."""
# Get the app root directory
app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
results = {}
all_available = True
for asset_name, paths in ASSET_PATHS.items():
available = True
missing = []
for path in paths:
full_path = os.path.join(app_root, path)
if not os.path.exists(full_path):
available = False
missing.append(path)
results[asset_name] = {
'available': available,
'missing': missing if not available else []
}
if not available:
all_available = False
return api_success(data={
'all_available': all_available,
'assets': results,
'offline_enabled': get_setting('offline.enabled', False)
})
@offline_bp.route('/check-asset', methods=['GET'])
def check_asset():
"""Check if a specific asset file exists."""
path = request.args.get('path', '')
if not path:
return api_error('Missing path parameter', 400)
# Security: only allow checking within static/vendor
if not path.startswith('/static/vendor/'):
return api_error('Invalid path', 400)
# Remove leading slash and construct full path
relative_path = path.lstrip('/')
app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
full_path = os.path.join(app_root, relative_path)
exists = os.path.exists(full_path)
return api_success(data={'path': path, 'exists': exists})