from fastapi import APIRouter, Depends, HTTPException, Request, Form, Body from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session import os import shutil import sqlite3 import logging from datetime import datetime from typing import Optional # Import from the database module from app.database import get_db, DATABASE_URL, engine from main import templates from app.models.llm_config import LLMConfig from pydantic import BaseModel router = APIRouter() class LLMConfigUpdate(BaseModel): openrouter_api_key: Optional[str] = None preferred_model: str browserless_api_key: Optional[str] = None def backup_database(source_db_path, backup_db_path): """Backs up an SQLite database using the online backup API.""" logging.info(f"DEBUG: Starting backup - source: {source_db_path}, backup: {backup_db_path}") import tempfile try: # Check if source database exists if not os.path.exists(source_db_path): logging.error(f"DEBUG: Source database file does not exist: {source_db_path}") return False # Create backup in temporary directory first (local fast storage) with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as temp_file: temp_backup_path = temp_file.name logging.info(f"DEBUG: Creating temporary backup at: {temp_backup_path}") # Backup to local temp file (fast) source_conn = sqlite3.connect(source_db_path) temp_conn = sqlite3.connect(temp_backup_path) with temp_conn: source_conn.backup(temp_conn) source_conn.close() temp_conn.close() logging.info(f"DEBUG: Temporary backup created, copying to final destination") # Ensure backup directory exists backup_dir = os.path.dirname(backup_db_path) if backup_dir and not os.path.exists(backup_dir): logging.info(f"DEBUG: Creating backup directory: {backup_dir}") os.makedirs(backup_dir, exist_ok=True) # Copy to NAS (this may be slow but won't block SQLite) shutil.copy2(temp_backup_path, backup_db_path) # Clean up temp file os.unlink(temp_backup_path) logging.info(f"Backup of '{source_db_path}' created successfully at '{backup_db_path}'") return True except sqlite3.Error as e: logging.error(f"SQLite error during backup: {e}", exc_info=True) return False except Exception as e: logging.error(f"Unexpected error during backup: {e}", exc_info=True) return False finally: # Cleanup temp file if it still exists if 'temp_backup_path' in locals() and os.path.exists(temp_backup_path): try: os.unlink(temp_backup_path) except: pass # Admin Section @router.get("/admin", response_class=HTMLResponse) async def admin_page(request: Request): return templates.TemplateResponse(request, "admin/index.html", {"request": request}) @router.get("/admin/imports", response_class=HTMLResponse) async def admin_imports_page(request: Request): return templates.TemplateResponse(request, "admin/imports.html", {"request": request}) @router.get("/admin/llm_config", response_class=HTMLResponse) async def admin_llm_config_page(request: Request, db: Session = Depends(get_db)): logging.info("DEBUG: Starting llm_config route") try: llm_config = db.query(LLMConfig).first() logging.info(f"DEBUG: LLMConfig query result: {llm_config}") if not llm_config: logging.info("DEBUG: No LLMConfig found, creating new one") llm_config = LLMConfig() db.add(llm_config) db.commit() db.refresh(llm_config) logging.info(f"DEBUG: Created new LLMConfig: {llm_config}") logging.info(f"DEBUG: Final llm_config object: {llm_config}") logging.info("DEBUG: About to render llm_config.html template") response = templates.TemplateResponse( request, "admin/llm_config.html", {"request": request, "llm_config": llm_config} ) logging.info("DEBUG: Template rendered successfully") return response except Exception as e: logging.error(f"DEBUG: Error in llm_config route: {e}", exc_info=True) raise @router.post("/admin/llm_config", response_class=RedirectResponse) async def update_llm_config( request: Request, openrouter_api_key: Optional[str] = Form(None), preferred_model: str = Form(...), browserless_api_key: Optional[str] = Form(None), db: Session = Depends(get_db) ): llm_config = db.query(LLMConfig).first() if not llm_config: llm_config = LLMConfig() db.add(llm_config) db.commit() db.refresh(llm_config) llm_config.openrouter_api_key = openrouter_api_key llm_config.preferred_model = preferred_model llm_config.browserless_api_key = browserless_api_key db.commit() db.refresh(llm_config) return RedirectResponse(url="/admin/llm_config", status_code=303) @router.get("/admin/backups", response_class=HTMLResponse) async def admin_backups_page(request: Request): BACKUP_DIR = "./backups" backups = [] if os.path.exists(BACKUP_DIR): backups = sorted( [f for f in os.listdir(BACKUP_DIR) if f.endswith(".db")], reverse=True ) return templates.TemplateResponse(request, "admin/backups.html", {"backups": backups}) @router.post("/admin/backups/create", response_class=HTMLResponse) async def create_backup(request: Request, db: Session = Depends(get_db)): db_path = DATABASE_URL.split("///")[1] timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") backup_dir = "./backups" # Ensure backup directory exists os.makedirs(backup_dir, exist_ok=True) backup_path = os.path.join(backup_dir, f"meal_planner_{timestamp}.db") backup_database(db_path, backup_path) # Redirect back to the backups page from fastapi.responses import RedirectResponse return RedirectResponse(url="/admin/backups", status_code=303) @router.post("/admin/backups/restore", response_class=HTMLResponse) async def restore_backup(request: Request, backup_file: str = Form(...)): import shutil BACKUP_DIR = "./backups" db_path = DATABASE_URL.split("///")[1] backup_path = os.path.join(BACKUP_DIR, backup_file) if not os.path.exists(backup_path): raise HTTPException(status_code=404, detail="Backup file not found.") try: # It's a good practice to close the current connection before overwriting the database engine.dispose() shutil.copyfile(backup_path, db_path) logging.info(f"Database restored from {backup_path}") except Exception as e: logging.error(f"Failed to restore backup: {e}") # You might want to add some user-facing error feedback here pass # Redirect back to the backups page from fastapi.responses import RedirectResponse return RedirectResponse(url="/admin/backups", status_code=303)