305 lines
9.2 KiB
Python
305 lines
9.2 KiB
Python
"""
|
|
System Settings API Routes
|
|
"""
|
|
from fastapi import APIRouter, HTTPException, Query, Depends, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func
|
|
from typing import Optional, Dict, Any, List
|
|
from datetime import datetime
|
|
import logging
|
|
from pydantic import BaseModel
|
|
|
|
from app.services.system_settings_service import system_settings_service
|
|
from app.services.redis_cache import redis_cache
|
|
from app.services.cache import cache_service
|
|
from app.database import get_db
|
|
from app.models.db import Position
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/system", tags=["system"])
|
|
|
|
|
|
# Pydantic models
|
|
class SettingCreate(BaseModel):
|
|
key: str
|
|
value: Any
|
|
value_type: str = "string"
|
|
category: str = "general"
|
|
label: str
|
|
description: Optional[str] = None
|
|
is_public: bool = False
|
|
|
|
|
|
class SettingUpdate(BaseModel):
|
|
value: Optional[Any] = None
|
|
value_type: Optional[str] = None
|
|
category: Optional[str] = None
|
|
label: Optional[str] = None
|
|
description: Optional[str] = None
|
|
is_public: Optional[bool] = None
|
|
|
|
|
|
# ============================================================
|
|
# System Settings CRUD APIs
|
|
# ============================================================
|
|
|
|
@router.get("/settings")
|
|
async def list_settings(
|
|
category: Optional[str] = Query(None, description="Filter by category"),
|
|
is_public: Optional[bool] = Query(None, description="Filter by public status"),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Get all system settings
|
|
|
|
Query parameters:
|
|
- category: Optional filter by category (e.g., 'visualization', 'cache', 'ui')
|
|
- is_public: Optional filter by public status (true for frontend-accessible settings)
|
|
"""
|
|
settings = await system_settings_service.get_all_settings(db, category, is_public)
|
|
|
|
result = []
|
|
for setting in settings:
|
|
# Parse value based on type
|
|
parsed_value = await system_settings_service.get_setting_value(setting.key, db)
|
|
|
|
result.append({
|
|
"id": setting.id,
|
|
"key": setting.key,
|
|
"value": parsed_value,
|
|
"raw_value": setting.value,
|
|
"value_type": setting.value_type,
|
|
"category": setting.category,
|
|
"label": setting.label,
|
|
"description": setting.description,
|
|
"is_public": setting.is_public,
|
|
"created_at": setting.created_at.isoformat() if setting.created_at else None,
|
|
"updated_at": setting.updated_at.isoformat() if setting.updated_at else None,
|
|
})
|
|
|
|
return {"settings": result}
|
|
|
|
|
|
@router.get("/settings/{key}")
|
|
async def get_setting(
|
|
key: str,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get a single setting by key"""
|
|
setting = await system_settings_service.get_setting(key, db)
|
|
|
|
if not setting:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Setting '{key}' not found"
|
|
)
|
|
|
|
parsed_value = await system_settings_service.get_setting_value(key, db)
|
|
|
|
return {
|
|
"id": setting.id,
|
|
"key": setting.key,
|
|
"value": parsed_value,
|
|
"raw_value": setting.value,
|
|
"value_type": setting.value_type,
|
|
"category": setting.category,
|
|
"label": setting.label,
|
|
"description": setting.description,
|
|
"is_public": setting.is_public,
|
|
"created_at": setting.created_at.isoformat() if setting.created_at else None,
|
|
"updated_at": setting.updated_at.isoformat() if setting.updated_at else None,
|
|
}
|
|
|
|
|
|
@router.post("/settings", status_code=status.HTTP_201_CREATED)
|
|
async def create_setting(
|
|
data: SettingCreate,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Create a new system setting"""
|
|
# Check if setting already exists
|
|
existing = await system_settings_service.get_setting(data.key, db)
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Setting '{data.key}' already exists"
|
|
)
|
|
|
|
new_setting = await system_settings_service.create_setting(data.dict(), db)
|
|
await db.commit()
|
|
|
|
parsed_value = await system_settings_service.get_setting_value(data.key, db)
|
|
|
|
return {
|
|
"id": new_setting.id,
|
|
"key": new_setting.key,
|
|
"value": parsed_value,
|
|
"value_type": new_setting.value_type,
|
|
"category": new_setting.category,
|
|
"label": new_setting.label,
|
|
"description": new_setting.description,
|
|
"is_public": new_setting.is_public,
|
|
}
|
|
|
|
|
|
@router.put("/settings/{key}")
|
|
async def update_setting(
|
|
key: str,
|
|
data: SettingUpdate,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Update a system setting"""
|
|
update_data = {k: v for k, v in data.dict().items() if v is not None}
|
|
|
|
updated = await system_settings_service.update_setting(key, update_data, db)
|
|
if not updated:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Setting '{key}' not found"
|
|
)
|
|
|
|
await db.commit()
|
|
|
|
parsed_value = await system_settings_service.get_setting_value(key, db)
|
|
|
|
return {
|
|
"id": updated.id,
|
|
"key": updated.key,
|
|
"value": parsed_value,
|
|
"value_type": updated.value_type,
|
|
"category": updated.category,
|
|
"label": updated.label,
|
|
"description": updated.description,
|
|
"is_public": updated.is_public,
|
|
}
|
|
|
|
|
|
@router.delete("/settings/{key}")
|
|
async def delete_setting(
|
|
key: str,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Delete a system setting"""
|
|
deleted = await system_settings_service.delete_setting(key, db)
|
|
if not deleted:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Setting '{key}' not found"
|
|
)
|
|
|
|
await db.commit()
|
|
|
|
return {"message": f"Setting '{key}' deleted successfully"}
|
|
|
|
|
|
# ============================================================
|
|
# Cache Management APIs
|
|
# ============================================================
|
|
|
|
@router.post("/cache/clear")
|
|
async def clear_all_caches():
|
|
"""
|
|
Clear all caches (memory + Redis)
|
|
|
|
This is a critical operation for platform management.
|
|
It clears:
|
|
- Memory cache (in-process)
|
|
- Redis cache (all positions and NASA data)
|
|
"""
|
|
logger.info("🧹 Starting cache clear operation...")
|
|
|
|
# Clear memory cache
|
|
cache_service.clear()
|
|
logger.info("✓ Memory cache cleared")
|
|
|
|
# Clear Redis cache
|
|
positions_cleared = await redis_cache.clear_pattern("positions:*")
|
|
nasa_cleared = await redis_cache.clear_pattern("nasa:*")
|
|
logger.info(f"✓ Redis cache cleared ({positions_cleared + nasa_cleared} keys)")
|
|
|
|
total_cleared = positions_cleared + nasa_cleared
|
|
|
|
return {
|
|
"message": f"All caches cleared successfully ({total_cleared} Redis keys deleted)",
|
|
"memory_cache": "cleared",
|
|
"redis_cache": {
|
|
"positions_keys": positions_cleared,
|
|
"nasa_keys": nasa_cleared,
|
|
"total": total_cleared
|
|
}
|
|
}
|
|
|
|
|
|
@router.get("/cache/stats")
|
|
async def get_cache_stats():
|
|
"""Get cache statistics"""
|
|
redis_stats = await redis_cache.get_stats()
|
|
|
|
return {
|
|
"redis": redis_stats,
|
|
"memory": {
|
|
"description": "In-memory cache (process-level)",
|
|
"note": "Statistics not available for in-memory cache"
|
|
}
|
|
}
|
|
|
|
|
|
@router.post("/settings/init-defaults")
|
|
async def initialize_default_settings(
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Initialize default system settings (admin use)"""
|
|
await system_settings_service.initialize_default_settings(db)
|
|
await db.commit()
|
|
|
|
return {"message": "Default settings initialized successfully"}
|
|
|
|
|
|
@router.get("/data-cutoff-date")
|
|
async def get_data_cutoff_date(
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Get the data cutoff date based on the Sun's (ID=10) last available data
|
|
|
|
This endpoint returns the latest date for which we have position data
|
|
in the database. It's used by the frontend to determine:
|
|
- The current date to display on the homepage
|
|
- The maximum date for timeline playback
|
|
|
|
Returns:
|
|
- cutoff_date: ISO format date string (YYYY-MM-DD)
|
|
- timestamp: Unix timestamp
|
|
- datetime: Full ISO datetime string
|
|
"""
|
|
try:
|
|
# Query the latest position data for the Sun (body_id = 10)
|
|
stmt = select(func.max(Position.time)).where(
|
|
Position.body_id == '10'
|
|
)
|
|
result = await db.execute(stmt)
|
|
latest_time = result.scalar_one_or_none()
|
|
|
|
if latest_time is None:
|
|
# No data available, return current date as fallback
|
|
logger.warning("No position data found for Sun (ID=10), using current date as fallback")
|
|
latest_time = datetime.utcnow()
|
|
|
|
# Format the response
|
|
cutoff_date = latest_time.strftime("%Y-%m-%d")
|
|
|
|
return {
|
|
"cutoff_date": cutoff_date,
|
|
"timestamp": int(latest_time.timestamp()),
|
|
"datetime": latest_time.isoformat(),
|
|
"message": "Data cutoff date retrieved successfully"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error retrieving data cutoff date: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to retrieve data cutoff date: {str(e)}"
|
|
)
|