cosmo_backend/app/api/system.py

305 lines
9.2 KiB
Python
Raw Permalink Normal View History

2025-12-02 06:29:38 +00:00
"""
System Settings API Routes
"""
from fastapi import APIRouter, HTTPException, Query, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession
2025-12-05 16:36:39 +00:00
from sqlalchemy import select, func
2025-12-02 06:29:38 +00:00
from typing import Optional, Dict, Any, List
2025-12-05 16:36:39 +00:00
from datetime import datetime
2025-12-02 06:29:38 +00:00
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
2025-12-05 16:36:39 +00:00
from app.models.db import Position
2025-12-02 06:29:38 +00:00
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"}
2025-12-05 16:36:39 +00:00
@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)}"
)