""" Offline mode routes - Asset management and settings for offline operation. """ import os 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") # 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": "", "offline.stadia_key": "", } # 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})