mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
feat(maps): add Stadia dark + tactical tile providers with API key support
- Add offline.stadia_key to OFFLINE_DEFAULTS in routes/offline.py - Add stadia_dark and tactical tile providers to Settings.tileProviders - Update getTileConfig() to inject Stadia API key or fall back to CartoDB dark - Add setStadiaKey() method for saving and applying the API key - Show/hide Stadia key row in setTileProvider() and _updateUI() - Add Stadia options to tile provider select in settings modal - Add Stadia API key input row to settings modal - Add TDD tests for stadia_key backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+53
-60
@@ -9,49 +9,43 @@ from flask import Blueprint, request
|
||||
from utils.database import get_setting, set_setting
|
||||
from utils.responses import api_error, api_success
|
||||
|
||||
offline_bp = Blueprint('offline', __name__, url_prefix='/offline')
|
||||
offline_bp = Blueprint("offline", __name__, url_prefix="/offline")
|
||||
|
||||
# Default offline settings
|
||||
OFFLINE_DEFAULTS = {
|
||||
'offline.enabled': False,
|
||||
"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': ''
|
||||
"offline.assets_source": "local",
|
||||
"offline.fonts_source": "local",
|
||||
"offline.tile_provider": "cartodb_dark_cyan",
|
||||
"offline.tile_server_url": "",
|
||||
"offline.stadia_key": "",
|
||||
}
|
||||
|
||||
# Asset paths to check
|
||||
ASSET_PATHS = {
|
||||
'leaflet': [
|
||||
'static/vendor/leaflet/leaflet.js',
|
||||
'static/vendor/leaflet/leaflet.css'
|
||||
"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",
|
||||
],
|
||||
'chartjs': [
|
||||
'static/vendor/chartjs/chart.umd.min.js'
|
||||
"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",
|
||||
],
|
||||
'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'
|
||||
"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",
|
||||
],
|
||||
'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'
|
||||
]
|
||||
"leaflet_heat": ["static/vendor/leaflet-heat/leaflet-heat.js"],
|
||||
}
|
||||
|
||||
|
||||
@@ -63,26 +57,26 @@ def get_offline_settings():
|
||||
return settings
|
||||
|
||||
|
||||
@offline_bp.route('/settings', methods=['GET'])
|
||||
@offline_bp.route("/settings", methods=["GET"])
|
||||
def get_settings():
|
||||
"""Get current offline settings."""
|
||||
settings = get_offline_settings()
|
||||
return api_success(data={'settings': settings})
|
||||
return api_success(data={"settings": settings})
|
||||
|
||||
|
||||
@offline_bp.route('/settings', methods=['POST'])
|
||||
@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)
|
||||
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']
|
||||
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)
|
||||
return api_error(f"Unknown setting: {key}", 400)
|
||||
|
||||
# Validate value type matches default
|
||||
default_type = type(OFFLINE_DEFAULTS[key])
|
||||
@@ -90,18 +84,18 @@ def save_setting():
|
||||
# Try to convert
|
||||
try:
|
||||
if default_type == bool:
|
||||
value = str(value).lower() in ('true', '1', 'yes')
|
||||
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)
|
||||
return api_error(f"Invalid value type for {key}", 400)
|
||||
|
||||
set_setting(key, value)
|
||||
|
||||
return api_success(data={'key': key, 'value': value})
|
||||
return api_success(data={"key": key, "value": value})
|
||||
|
||||
|
||||
@offline_bp.route('/status', methods=['GET'])
|
||||
@offline_bp.route("/status", methods=["GET"])
|
||||
def get_status():
|
||||
"""Check status of local assets."""
|
||||
# Get the app root directory
|
||||
@@ -119,37 +113,36 @@ def get_status():
|
||||
available = False
|
||||
missing.append(path)
|
||||
|
||||
results[asset_name] = {
|
||||
'available': available,
|
||||
'missing': missing if not available else []
|
||||
}
|
||||
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)
|
||||
})
|
||||
return api_success(
|
||||
data={
|
||||
"all_available": all_available,
|
||||
"assets": results,
|
||||
"offline_enabled": get_setting("offline.enabled", False),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@offline_bp.route('/check-asset', methods=['GET'])
|
||||
@offline_bp.route("/check-asset", methods=["GET"])
|
||||
def check_asset():
|
||||
"""Check if a specific asset file exists."""
|
||||
path = request.args.get('path', '')
|
||||
path = request.args.get("path", "")
|
||||
if not path:
|
||||
return api_error('Missing path parameter', 400)
|
||||
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)
|
||||
if not path.startswith("/static/vendor/"):
|
||||
return api_error("Invalid path", 400)
|
||||
|
||||
# Remove leading slash and construct full path
|
||||
relative_path = path.lstrip('/')
|
||||
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})
|
||||
return api_success(data={"path": path, "exists": exists})
|
||||
|
||||
Reference in New Issue
Block a user