diff --git a/.env.example b/.env.example index ec98ec7..35374b2 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,8 @@ SHOW_THINKING_PROCESS=true BASE_URL=https://generativelanguage.googleapis.com/v1beta MAX_FAILURES=10 MAX_RETRIES=3 +CHECK_INTERVAL_HOURS=1 +TIMEZONE=Asia/Shanghai # 请求超时时间(秒) TIME_OUT=300 #########################image_generate 相关配置########################### diff --git a/app/config/config.py b/app/config/config.py index 8cf2d5b..7043cb3 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -62,7 +62,11 @@ class Settings(BaseSettings): STREAM_SHORT_TEXT_THRESHOLD: int = DEFAULT_STREAM_SHORT_TEXT_THRESHOLD STREAM_LONG_TEXT_THRESHOLD: int = DEFAULT_STREAM_LONG_TEXT_THRESHOLD STREAM_CHUNK_SIZE: int = DEFAULT_STREAM_CHUNK_SIZE - + + # 调度器配置 + CHECK_INTERVAL_HOURS: int = 1 # 默认检查间隔为1小时 + TIMEZONE: str = "Asia/Shanghai" # 默认时区 + def __init__(self, **kwargs): super().__init__(**kwargs) # 设置默认AUTH_TOKEN(如果未提供) diff --git a/app/core/application.py b/app/core/application.py index b63fe42..d465306 100644 --- a/app/core/application.py +++ b/app/core/application.py @@ -14,6 +14,7 @@ from app.service.key.key_manager import get_key_manager_instance from app.core.initialization import initialize_app from app.database.connection import connect_to_db, disconnect_from_db from app.database.initialization import initialize_database +from app.scheduler.key_checker import start_scheduler, stop_scheduler # 导入调度器函数 logger = get_application_logger() @@ -44,12 +45,20 @@ async def lifespan(app: FastAPI): except Exception as e: logger.error(f"Failed to initialize application: {str(e)}") raise - + + # 启动调度器 + start_scheduler() + logger.info("Scheduler started successfully.") + yield # 应用程序运行期间 # 关闭事件 logger.info("Application shutting down...") + # 停止调度器 + stop_scheduler() + logger.info("Scheduler stopped.") + # 断开数据库连接 await disconnect_from_db() diff --git a/app/database/services.py b/app/database/services.py index f73cd96..b14a814 100644 --- a/app/database/services.py +++ b/app/database/services.py @@ -1,10 +1,9 @@ """ 数据库服务模块 """ -import datetime import json from typing import Dict, List, Optional, Any, Union -from datetime import date, timedelta # Import date and timedelta +from datetime import datetime from sqlalchemy import select, insert, update, func # Import func for COUNT @@ -142,7 +141,7 @@ async def add_error_log( model_name=model_name, error_code=error_code, request_msg=request_msg_json, - request_time=datetime.datetime.now() + request_time=datetime.now() ) ) await database.execute(query) @@ -158,8 +157,8 @@ async def get_error_logs( offset: int = 0, key_search: Optional[str] = None, error_search: Optional[str] = None, - start_date: Optional[date] = None, - end_date: Optional[date] = None + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None ) -> List[Dict[str, Any]]: """ 获取错误日志,支持搜索和日期过滤 @@ -169,8 +168,8 @@ async def get_error_logs( offset (int): 偏移量 key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配) error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配) - start_date (Optional[date]): 开始日期 - end_date (Optional[date]): 结束日期 + start_date (Optional[datetime]): 开始日期时间 + end_date (Optional[datetime]): 结束日期时间 Returns: List[Dict[str, Any]]: 错误日志列表 @@ -189,8 +188,8 @@ async def get_error_logs( if start_date: query = query.where(ErrorLog.request_time >= start_date) if end_date: - # Add 1 day to end_date to include the whole day - query = query.where(ErrorLog.request_time < end_date + timedelta(days=1)) + # Use the datetime object directly for comparison + query = query.where(ErrorLog.request_time < end_date) # Apply ordering, limit, and offset query = query.order_by(ErrorLog.request_time.desc()).limit(limit).offset(offset) @@ -205,8 +204,8 @@ async def get_error_logs( async def get_error_logs_count( key_search: Optional[str] = None, error_search: Optional[str] = None, - start_date: Optional[date] = None, - end_date: Optional[date] = None + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None ) -> int: """ 获取符合条件的错误日志总数 @@ -214,8 +213,8 @@ async def get_error_logs_count( Args: key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配) error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配) - start_date (Optional[date]): 开始日期 - end_date (Optional[date]): 结束日期 + start_date (Optional[datetime]): 开始日期时间 + end_date (Optional[datetime]): 结束日期时间 Returns: int: 日志总数 @@ -234,7 +233,8 @@ async def get_error_logs_count( if start_date: query = query.where(ErrorLog.request_time >= start_date) if end_date: - query = query.where(ErrorLog.request_time < end_date + timedelta(days=1)) + # Use the datetime object directly for comparison + query = query.where(ErrorLog.request_time < end_date) count_result = await database.fetch_one(query) return count_result[0] if count_result else 0 diff --git a/app/router/gemini_routes.py b/app/router/gemini_routes.py index 3d09871..e879bb4 100644 --- a/app/router/gemini_routes.py +++ b/app/router/gemini_routes.py @@ -146,15 +146,67 @@ async def stream_generate_content( logger.error(f"Streaming request failed: {str(e)}") raise HTTPException(status_code=500, detail="Streaming request failed") from e +@router.post("/reset-all-fail-counts") +async def reset_all_key_fail_counts(key_type: str = None, key_manager: KeyManager = Depends(get_key_manager)): + """批量重置Gemini API密钥的失败计数,可选择性地仅重置有效或无效密钥""" + logger.info("-" * 50 + "reset_all_gemini_key_fail_counts" + "-" * 50) + logger.info(f"Received reset request with key_type: {key_type}") + + try: + # 获取分类后的密钥 + keys_by_status = await key_manager.get_keys_by_status() + valid_keys = keys_by_status.get("valid_keys", {}) + invalid_keys = keys_by_status.get("invalid_keys", {}) + + # 根据类型选择要重置的密钥 + keys_to_reset = [] + if key_type == "valid": + keys_to_reset = list(valid_keys.keys()) + logger.info(f"Resetting only valid keys, count: {len(keys_to_reset)}") + elif key_type == "invalid": + keys_to_reset = list(invalid_keys.keys()) + logger.info(f"Resetting only invalid keys, count: {len(keys_to_reset)}") + else: + # 重置所有密钥 + await key_manager.reset_failure_counts() + return JSONResponse({"success": True, "message": "所有密钥的失败计数已重置"}) + + # 批量重置指定类型的密钥 + for key in keys_to_reset: + await key_manager.reset_key_failure_count(key) + + return JSONResponse({ + "success": True, + "message": f"{key_type}密钥的失败计数已重置", + "reset_count": len(keys_to_reset) + }) + except Exception as e: + logger.error(f"Failed to reset key failure counts: {str(e)}") + return JSONResponse({"success": False, "message": f"批量重置失败: {str(e)}"}, status_code=500) + + +@router.post("/reset-fail-count/{api_key}") +async def reset_key_fail_count(api_key: str, key_manager: KeyManager = Depends(get_key_manager)): + """重置指定Gemini API密钥的失败计数""" + logger.info("-" * 50 + "reset_gemini_key_fail_count" + "-" * 50) + logger.info(f"Resetting failure count for API key: {api_key}") + + try: + result = await key_manager.reset_key_failure_count(api_key) + if result: + return JSONResponse({"success": True, "message": "失败计数已重置"}) + return JSONResponse({"success": False, "message": "未找到指定密钥"}, status_code=404) + except Exception as e: + logger.error(f"Failed to reset key failure count: {str(e)}") + return JSONResponse({"success": False, "message": f"重置失败: {str(e)}"}, status_code=500) @router.post("/verify-key/{api_key}") -async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get_chat_service)): +async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get_chat_service), key_manager: KeyManager = Depends(get_key_manager)): """验证Gemini API密钥的有效性""" logger.info("-" * 50 + "verify_gemini_key" + "-" * 50) logger.info("Verifying API key validity") try: - # 使用generate_content接口测试key的有效性 gemini_request = GeminiRequest( contents=[ @@ -167,13 +219,19 @@ async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get response = await chat_service.generate_content( settings.TEST_MODEL, - gemini_request, + gemini_request, api_key ) if response: - return JSONResponse({"status": "valid"}) - return JSONResponse({"status": "invalid"}) + return JSONResponse({"status": "valid"}) except Exception as e: logger.error(f"Key verification failed: {str(e)}") + + # 验证出现异常时增加失败计数 + async with key_manager.failure_count_lock: + if api_key in key_manager.key_failure_counts: + key_manager.key_failure_counts[api_key] += 1 + logger.warning(f"Verification exception for key: {api_key}, incrementing failure count") + return JSONResponse({"status": "invalid", "error": str(e)}) \ No newline at end of file diff --git a/app/router/log_routes.py b/app/router/log_routes.py index bbd6b7b..0f3d56c 100644 --- a/app/router/log_routes.py +++ b/app/router/log_routes.py @@ -2,7 +2,7 @@ 日志路由模块 """ from typing import Any, Dict, List, Optional -from datetime import date +from datetime import datetime from pydantic import BaseModel from fastapi import APIRouter, HTTPException, Request, Query from fastapi.responses import RedirectResponse @@ -29,8 +29,8 @@ async def get_error_logs_api( offset: int = Query(0, ge=0), key_search: Optional[str] = Query(None, description="Search term for Gemini key (partial match)"), error_search: Optional[str] = Query(None, description="Search term for error type or log message"), - start_date: Optional[date] = Query(None, description="Start date for filtering (YYYY-MM-DD)"), - end_date: Optional[date] = Query(None, description="End date for filtering (YYYY-MM-DD)") + start_date: Optional[datetime] = Query(None, description="Start datetime for filtering (YYYY-MM-DDTHH:MM)"), + end_date: Optional[datetime] = Query(None, description="End datetime for filtering (YYYY-MM-DDTHH:MM)") ): """ 获取错误日志 diff --git a/app/router/routes.py b/app/router/routes.py index bc36739..68712dc 100644 --- a/app/router/routes.py +++ b/app/router/routes.py @@ -8,7 +8,7 @@ from fastapi.templating import Jinja2Templates from app.core.security import verify_auth_token from app.log.logger import get_routes_logger -from app.router import gemini_routes, openai_routes, config_routes, log_routes +from app.router import gemini_routes, openai_routes, config_routes, log_routes, scheduler_routes # 新增导入 from app.service.key.key_manager import get_key_manager_instance logger = get_routes_logger() @@ -30,6 +30,7 @@ def setup_routers(app: FastAPI) -> None: app.include_router(gemini_routes.router_v1beta) app.include_router(config_routes.router) app.include_router(log_routes.router) + app.include_router(scheduler_routes.router) # 新增包含 scheduler 路由 # 添加页面路由 setup_page_routes(app) diff --git a/app/router/scheduler_routes.py b/app/router/scheduler_routes.py new file mode 100644 index 0000000..91189d5 --- /dev/null +++ b/app/router/scheduler_routes.py @@ -0,0 +1,63 @@ +""" +定时任务控制路由模块 +""" + +from fastapi import APIRouter, Request, HTTPException, status # 移除 Depends, 添加 Request +from fastapi.responses import JSONResponse + +from app.core.security import verify_auth_token # 导入 verify_auth_token +from app.scheduler.key_checker import start_scheduler, stop_scheduler +from app.log.logger import get_routes_logger # 使用路由日志记录器 + +logger = get_routes_logger() + +router = APIRouter( + prefix="/api/scheduler", + tags=["Scheduler"] + # 移除全局依赖 +) + +# 认证检查的辅助函数 +async def verify_token(request: Request): + auth_token = request.cookies.get("auth_token") + if not auth_token or not verify_auth_token(auth_token): + logger.warning("Unauthorized access attempt to scheduler API") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + +@router.post("/start", summary="启动定时任务") +async def start_scheduler_endpoint(request: Request): # 添加 request 参数 + """Start the background scheduler task""" + """ + await verify_token(request) # 在函数开始处进行认证检查 + """ + try: + logger.info("Received request to start scheduler.") + start_scheduler() # 调用 key_checker 中的函数 + return JSONResponse(content={"message": "Scheduler started successfully."}, status_code=status.HTTP_200_OK) + except Exception as e: + logger.error(f"Error starting scheduler: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to start scheduler: {str(e)}" + ) + +@router.post("/stop", summary="停止定时任务") +async def stop_scheduler_endpoint(request: Request): # 添加 request 参数 + """Stop the background scheduler task""" + """ + await verify_token(request) # 在函数开始处进行认证检查 + """ + try: + logger.info("Received request to stop scheduler.") + stop_scheduler() # 调用 key_checker 中的函数 + return JSONResponse(content={"message": "Scheduler stopped successfully."}, status_code=status.HTTP_200_OK) + except Exception as e: + logger.error(f"Error stopping scheduler: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to stop scheduler: {str(e)}" + ) \ No newline at end of file diff --git a/app/scheduler/key_checker.py b/app/scheduler/key_checker.py new file mode 100644 index 0000000..4dd0b0d --- /dev/null +++ b/app/scheduler/key_checker.py @@ -0,0 +1,100 @@ +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from app.service.key.key_manager import get_key_manager_instance +from app.service.chat.gemini_chat_service import GeminiChatService +from app.domain.gemini_models import GeminiRequest, GeminiContent +from app.config.config import settings +from app.log.logger import Logger # 导入 Logger 类 + +logger = Logger.setup_logger("scheduler") # 使用 Logger.setup_logger + +async def check_failed_keys(): + """ + 定时检查失败次数大于0的API密钥,并尝试验证它们。 + 如果验证成功,重置失败计数;如果失败,增加失败计数。 + """ + logger.info("Starting scheduled check for failed API keys...") + try: + key_manager = await get_key_manager_instance() + # 确保 KeyManager 已经初始化 + if not key_manager or not hasattr(key_manager, 'key_failure_counts'): + logger.warning("KeyManager instance not available or not initialized. Skipping check.") + return + + # 创建 GeminiChatService 实例用于验证 + # 注意:这里直接创建实例,而不是通过依赖注入,因为这是后台任务 + chat_service = GeminiChatService(settings.BASE_URL, key_manager) + + # 获取需要检查的 key 列表 (失败次数 > 0) + keys_to_check = [] + async with key_manager.failure_count_lock: # 访问共享数据需要加锁 + # 复制一份以避免在迭代时修改字典 + failure_counts_copy = key_manager.key_failure_counts.copy() + keys_to_check = [key for key, count in failure_counts_copy.items() if count > 0] # 检查所有失败次数大于0的key + + if not keys_to_check: + logger.info("No keys with failure count > 0 found. Skipping verification.") + return + + logger.info(f"Found {len(keys_to_check)} keys with failure count > 0 to verify.") + + for key in keys_to_check: + # 隐藏部分 key 用于日志记录 + log_key = f"{key[:4]}...{key[-4:]}" if len(key) > 8 else key + logger.info(f"Verifying key: {log_key}...") + try: + # 构造测试请求 + gemini_request = GeminiRequest( + contents=[ + GeminiContent( + role="user", + parts=[{"text": "hi"}] # 使用简单的文本进行验证 + ) + ] + ) + # 调用 generate_content 进行验证 + await chat_service.generate_content( + settings.TEST_MODEL, # 使用配置中定义的测试模型 + gemini_request, + key + ) + # 如果没有抛出异常,说明 key 有效 + logger.info(f"Key {log_key} verification successful. Resetting failure count.") + await key_manager.reset_key_failure_count(key) + except Exception as e: + # 验证失败,增加失败计数 + logger.warning(f"Key {log_key} verification failed: {str(e)}. Incrementing failure count.") + # 直接操作计数器,需要加锁 + async with key_manager.failure_count_lock: + # 再次检查 key 是否存在且失败次数未达上限 + if key in key_manager.key_failure_counts and key_manager.key_failure_counts[key] < key_manager.MAX_FAILURES: + key_manager.key_failure_counts[key] += 1 + logger.info(f"Failure count for key {log_key} incremented to {key_manager.key_failure_counts[key]}.") + elif key in key_manager.key_failure_counts: + logger.warning(f"Key {log_key} reached MAX_FAILURES ({key_manager.MAX_FAILURES}). Not incrementing further.") + + + except Exception as e: + logger.error(f"An error occurred during the scheduled key check: {str(e)}", exc_info=True) + +def setup_scheduler(): + """设置并启动 APScheduler""" + scheduler = AsyncIOScheduler(timezone=str(settings.TIMEZONE)) # 从配置读取时区 + # 添加定时任务,例如每小时执行一次 (可以调整) + scheduler.add_job(check_failed_keys, 'interval', hours=settings.CHECK_INTERVAL_HOURS) + scheduler.start() + logger.info(f"Scheduler started. Key check job scheduled to run every {settings.CHECK_INTERVAL_HOURS} hour(s).") + return scheduler + +# 可以在这里添加一个全局的 scheduler 实例,以便在应用关闭时优雅地停止 +scheduler_instance = None + +def start_scheduler(): + global scheduler_instance + if scheduler_instance is None: + scheduler_instance = setup_scheduler() + +def stop_scheduler(): + global scheduler_instance + if scheduler_instance and scheduler_instance.running: + scheduler_instance.shutdown() + logger.info("Scheduler stopped.") \ No newline at end of file diff --git a/app/service/key/key_manager.py b/app/service/key/key_manager.py index 4f27ffa..ec87db7 100644 --- a/app/service/key/key_manager.py +++ b/app/service/key/key_manager.py @@ -37,6 +37,16 @@ class KeyManager: async with self.failure_count_lock: for key in self.key_failure_counts: self.key_failure_counts[key] = 0 + + async def reset_key_failure_count(self, key: str) -> bool: + """重置指定key的失败计数""" + async with self.failure_count_lock: + if key in self.key_failure_counts: + self.key_failure_counts[key] = 0 + logger.info(f"Reset failure count for key: {key}") + return True + logger.warning(f"Attempt to reset failure count for non-existent key: {key}") + return False async def get_next_working_key(self) -> str: """获取下一可用的API key""" diff --git a/app/static/css/auth.css b/app/static/css/auth.css deleted file mode 100644 index 12571ce..0000000 --- a/app/static/css/auth.css +++ /dev/null @@ -1,249 +0,0 @@ -body { - font-family: 'Roboto', sans-serif; - line-height: 1.6; - margin: 0; - padding: 0; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - min-height: 100vh; - display: flex; - justify-content: center; - align-items: center; -} - -.container { - max-width: 400px; - width: 90%; - background: rgba(255, 255, 255, 0.95); - padding: 40px; - border-radius: 20px; - box-shadow: 0 15px 35px rgba(0,0,0,0.2); - backdrop-filter: blur(10px); - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); -} - -.container:hover { - transform: translateY(-5px); - box-shadow: 0 20px 40px rgba(0,0,0,0.25); -} - -.logo { - text-align: center; - margin-bottom: 30px; - animation: fadeIn 1s ease; -} - -.logo i { - font-size: 48px; - color: #764ba2; - margin-bottom: 15px; -} - -h2 { - color: #2c3e50; - text-align: center; - margin-bottom: 30px; - font-weight: 700; - font-size: 24px; - animation: slideDown 0.5s ease; -} - -form { - display: flex; - flex-direction: column; - gap: 20px; -} - -.input-group { - position: relative; - animation: slideUp 0.5s ease; -} - -.input-group i { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - color: #764ba2; - font-size: 18px; -} - -input { - width: 100%; - padding: 12px 12px 12px 40px; - border: 2px solid #e0e0e0; - border-radius: 10px; - font-size: 16px; - box-sizing: border-box; - transition: all 0.3s ease; - background: rgba(255, 255, 255, 0.9); -} - -input:focus { - border-color: #764ba2; - box-shadow: 0 0 10px rgba(118, 75, 162, 0.2); - outline: none; -} - -button { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border: none; - padding: 14px; - border-radius: 10px; - cursor: pointer; - font-size: 16px; - font-weight: bold; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -button:hover { - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3); -} - -button:active { - transform: translateY(0); -} - -button::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - transform: translate(-50%, -50%); - transition: width 0.6s, height 0.6s; -} - -button:active::after { - width: 200px; - height: 200px; - opacity: 0; -} - -.error-message { - color: #e74c3c; - margin-top: 15px; - text-align: center; - font-weight: bold; - padding: 10px; - border-radius: 5px; - background: rgba(231, 76, 60, 0.1); - animation: shake 0.5s ease; -} - -.copyright { - position: fixed; - bottom: 0; - left: 0; - width: 100%; - background: rgba(255, 255, 255, 0.9); - padding: 10px 0; - text-align: center; - font-size: 14px; - color: #2c3e50; - backdrop-filter: blur(5px); - border-top: 1px solid rgba(0,0,0,0.1); -} - -.copyright a { - color: #764ba2; - text-decoration: none; - transition: color 0.3s ease; -} - -.copyright a:hover { - color: #667eea; -} - -.copyright img { - width: 20px; - height: 20px; - border-radius: 50%; - vertical-align: middle; - margin-right: 5px; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideDown { - from { transform: translateY(-20px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } -} - -@keyframes slideUp { - from { transform: translateY(20px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } -} - -@keyframes shake { - 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-5px); } - 75% { transform: translateX(5px); } -} - -@media (max-width: 768px) { - .container { - width: 85%; - padding: 30px; - } - .logo i { - font-size: 40px; - } - h2 { - font-size: 22px; - } - input { - padding: 10px 10px 10px 35px; - font-size: 15px; - } - .input-group i { - font-size: 16px; - } - button { - padding: 12px; - font-size: 15px; - } -} - -@media (max-width: 480px) { - .container { - width: 90%; - padding: 25px; - } - .logo i { - font-size: 36px; - } - h2 { - font-size: 20px; - margin-bottom: 25px; - } - form { - gap: 15px; - } - input { - padding: 10px 10px 10px 32px; - font-size: 14px; - } - .input-group i { - font-size: 15px; - left: 10px; - } - button { - padding: 10px; - font-size: 14px; - } - .error-message { - font-size: 14px; - padding: 8px; - margin-top: 12px; - } -} diff --git a/app/static/css/config_editor.css b/app/static/css/config_editor.css deleted file mode 100644 index 87f97f8..0000000 --- a/app/static/css/config_editor.css +++ /dev/null @@ -1,794 +0,0 @@ -* { - box-sizing: border-box; -} - -body { - font-family: 'Roboto', sans-serif; - line-height: 1.6; - margin: 0; - padding: 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - min-height: 100vh; -} - -.container { - max-width: 900px; - width: 95%; - background: rgba(255, 255, 255, 0.95); - padding: 40px; - border-radius: 20px; - box-shadow: 0 15px 35px rgba(0,0,0,0.2); - backdrop-filter: blur(10px); - position: relative; - margin: 20px auto; - overflow-y: auto; - max-height: calc(100vh - 40px); - scrollbar-width: none; - -ms-overflow-style: none; -} - -.container::-webkit-scrollbar { - display: none; -} - -h1 { - color: #2c3e50; - text-align: center; - margin-bottom: 30px; - font-weight: 700; - font-size: 32px; - position: relative; - padding-bottom: 15px; -} - -h1::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 100px; - height: 4px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 2px; -} - -h2 { - color: #2c3e50; - margin-bottom: 20px; - font-size: 1.5em; - padding-bottom: 10px; - border-bottom: 2px solid rgba(0,0,0,0.1); - display: flex; - align-items: center; - gap: 10px; -} - -/* 导航标签样式 */ -.nav-tabs { - display: flex; - justify-content: center; - margin-bottom: 30px; - border-bottom: 2px solid rgba(0,0,0,0.1); - padding-bottom: 10px; - width: 100%; - max-width: 500px; - margin-left: auto; - margin-right: auto; -} - -.tab-link { - color: #2c3e50; - text-decoration: none; - padding: 12px 25px; - border-radius: 8px 8px 0 0; - font-weight: bold; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - position: relative; - margin: 0 5px; - flex: 1; - text-align: center; -} - -.tab-link:hover { - background: rgba(102, 126, 234, 0.1); -} - -.tab-link.active { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3); -} - -.tab-link.active::after { - content: ''; - position: absolute; - bottom: -12px; - left: 0; - width: 100%; - height: 4px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 2px; -} - -.tab-link i { - font-size: 16px; -} - -.config-tabs { - display: flex; - justify-content: center; - margin-bottom: 30px; - flex-wrap: wrap; - gap: 10px; -} - -.tab-btn { - background: rgba(255, 255, 255, 0.7); - border: 1px solid rgba(0,0,0,0.1); - padding: 10px 20px; - border-radius: 30px; - cursor: pointer; - font-weight: bold; - transition: all 0.3s ease; - color: #2c3e50; - font-size: 14px; -} - -.tab-btn:hover { - background: rgba(255, 255, 255, 0.9); - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.1); -} - -.tab-btn.active { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3); -} - -.config-section { - display: none; - animation: fadeIn 0.5s ease forwards; - background: rgba(248, 249, 250, 0.9); - padding: 25px; - border-radius: 15px; - margin-bottom: 30px; - border: 1px solid rgba(0,0,0,0.1); -} - -.config-section.active { - display: block; -} - -.config-item { - margin-bottom: 25px; - position: relative; -} - -.config-item label { - display: block; - margin-bottom: 8px; - font-weight: bold; - color: #2c3e50; -} - -.config-item input[type="text"], -.config-item input[type="number"], -.config-item select { - width: 100%; - padding: 12px 15px; - border: 1px solid rgba(0,0,0,0.1); - border-radius: 8px; - font-size: 16px; - transition: all 0.3s ease; - background: white; - box-sizing: border-box; -} - -.config-item input[type="text"]:focus, -.config-item input[type="number"]:focus, -.config-item select:focus { - border-color: #667eea; - box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); - outline: none; -} - -.help-text { - display: block; - margin-top: 5px; - font-size: 12px; - color: #7f8c8d; -} - -.array-container { - border: 1px solid rgba(0,0,0,0.1); - border-radius: 8px; - padding: 15px; - background: white; - /* margin-bottom: 10px; */ /* Removed as controls are now outside */ - width: 100%; - min-height: 60px; -} - -/* Specific style for API Keys container to make it scrollable */ -#API_KEYS_container { - max-height: 300px; /* Adjust this value as needed */ - overflow-y: auto; - /* Optional: Add some padding to the right for the scrollbar */ - padding-right: 5px; - margin-bottom: 10px; /* Add margin below the container */ -} - -/* Search Input Styles */ -.search-container { - margin-bottom: 10px; -} - -#apiKeySearchInput { - width: 100%; - padding: 10px 15px; - border: 1px solid rgba(0,0,0,0.1); - border-radius: 8px; - font-size: 14px; - box-sizing: border-box; -} - -#apiKeySearchInput:focus { - border-color: #667eea; - box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); - outline: none; -} - - -.array-item { - display: flex; - margin-bottom: 10px; - gap: 10px; - background-color: rgba(248, 249, 250, 0.5); - padding: 8px; - border-radius: 8px; - border: 1px dashed rgba(102, 126, 234, 0.3); - transition: all 0.3s ease; -} - -.array-item:hover { - background-color: rgba(248, 249, 250, 0.8); - border-color: rgba(102, 126, 234, 0.5); - box-shadow: 0 2px 8px rgba(0,0,0,0.05); -} - -.array-item input { - flex: 1; - box-sizing: border-box; - padding: 12px 15px; - border: 1px solid rgba(0,0,0,0.1); - border-radius: 8px; - font-size: 16px; - transition: all 0.3s ease; - background: white; -} - -.array-item input:focus { - border-color: #667eea; - box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); - outline: none; -} - -.array-controls { - display: flex; - justify-content: flex-end; - margin-top: 10px; /* Increase margin-top for spacing */ -} - -.add-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border: none; - padding: 8px 15px; - border-radius: 8px; - cursor: pointer; - font-size: 14px; - font-weight: bold; - transition: all 0.3s ease; - display: flex; - align-items: center; - gap: 5px; -} - -.add-btn:hover { - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3); -} - -.remove-btn { - background: #e74c3c; - color: white; - border: none; - width: 30px; - height: 30px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.3s ease; -} - -.remove-btn:hover { - background: #c0392b; - transform: scale(1.1); -} - -.toggle { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: nowrap; -} - -.toggle label { - margin-bottom: 0; - flex: 1; - padding-right: 15px; -} - -.toggle-switch { - position: relative; - display: inline-block; - width: 60px; - height: 34px; - flex-shrink: 0; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.toggle-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - transition: .4s; - border-radius: 34px; -} - -.toggle-slider:before { - position: absolute; - content: ""; - height: 26px; - width: 26px; - left: 4px; - bottom: 4px; - background-color: white; - transition: .4s; - border-radius: 50%; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); -} - -input:checked + .toggle-slider { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -} - -input:focus + .toggle-slider { - box-shadow: 0 0 1px #667eea; -} - -input:checked + .toggle-slider:before { - transform: translateX(26px); -} - -.form-actions { - display: flex; - justify-content: center; - gap: 20px; - margin-top: 40px; -} - -.save-btn, .reset-btn { - padding: 12px 30px; - border-radius: 30px; - font-size: 16px; - font-weight: bold; - cursor: pointer; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - border: none; - box-shadow: 0 4px 10px rgba(0,0,0,0.1); -} - -.save-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; -} - -.save-btn:hover { - transform: translateY(-3px); - box-shadow: 0 10px 20px rgba(118, 75, 162, 0.3); - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); -} - -.save-btn:active { - transform: translateY(1px); - box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2); -} - -.reset-btn { - background: linear-gradient(135deg, #e0e0e0 0%, #bdc3c7 100%); - color: #2c3e50; -} - -.reset-btn:hover { - background: linear-gradient(135deg, #bdc3c7 0%, #e0e0e0 100%); - transform: translateY(-3px); - box-shadow: 0 10px 20px rgba(0,0,0,0.1); -} - -.reset-btn:active { - transform: translateY(1px); - box-shadow: 0 5px 10px rgba(0,0,0,0.05); -} - -.save-status { - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - background: rgba(39, 174, 96, 0.9); - color: white; - padding: 10px 20px; - border-radius: 30px; - display: flex; - align-items: center; - gap: 10px; - font-weight: bold; - box-shadow: 0 5px 15px rgba(0,0,0,0.2); - z-index: 1000; - opacity: 0; - transition: all 0.3s ease; -} - -.save-status.show { - opacity: 1; -} - -.save-status.error { - background: rgba(231, 76, 60, 0.9); -} - -.provider-config { - display: none; -} - -.provider-config.active { - display: block; -} - -.notification { - position: fixed; - bottom: 20px; - right: 20px; - padding: 15px 25px; - border-radius: 8px; - background: rgba(0,0,0,0.8); - color: white; - font-weight: bold; - box-shadow: 0 5px 15px rgba(0,0,0,0.2); - z-index: 1000; - opacity: 0; - transform: translateY(20px); - transition: all 0.3s ease; -} - -.notification.show { - opacity: 1; - transform: translateY(0); -} - -.notification.success { - background: rgba(39, 174, 96, 0.9); -} - -.notification.error { - background: rgba(231, 76, 60, 0.9); -} - -.scroll-buttons { - position: fixed; - right: 20px; - bottom: 20px; - display: flex; - flex-direction: column; - gap: 10px; - z-index: 1000; -} - -.scroll-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - width: 40px; - height: 40px; - border: none; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; - transition: all 0.3s ease; - backdrop-filter: blur(5px); - box-shadow: 0 2px 10px rgba(0,0,0,0.2); -} - -.scroll-btn:hover { - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); - transform: scale(1.1); -} - -.scroll-btn:active { - transform: scale(0.95); -} - -.refresh-btn { - position: fixed; - top: 20px; - right: 20px; - z-index: 1000; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: #fff; - border: none; - padding: 10px 20px; - border-radius: 25px; - cursor: pointer; - font-size: 14px; - font-weight: bold; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); -} - -.refresh-btn:hover { - transform: scale(1.05); - box-shadow: 0 8px 20px rgba(118, 75, 162, 0.3); - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); -} - -.refresh-btn:active { - transform: scale(0.95); -} - -.refresh-btn i { - transition: transform 0.5s ease; -} - -.refresh-btn.loading i { - animation: spin 1s linear infinite; -} - -.copyright { - position: fixed; - bottom: 0; - left: 0; - width: 100%; - background: rgba(255, 255, 255, 0.9); - padding: 10px 0; - text-align: center; - font-size: 14px; - color: #2c3e50; - backdrop-filter: blur(5px); - border-top: 1px solid rgba(0,0,0,0.1); -} - -.copyright a { - color: #764ba2; - text-decoration: none; - transition: color 0.3s ease; -} - -.copyright a:hover { - color: #667eea; -} - -.copyright img { - width: 20px; - height: 20px; - border-radius: 50%; - vertical-align: middle; - margin-right: 5px; -} - -/* Modal Styles */ -.modal { - display: none; /* Hidden by default */ - position: fixed; /* Change back to fixed */ - z-index: 1001; /* Sit on top */ - left: 0; - top: 0; - width: 100%; /* Full width */ - height: 100%; /* Full height */ - /* overflow: auto; Removed */ - background-color: rgba(0,0,0,0.6); /* Black w/ opacity */ - /* backdrop-filter: blur(5px); Removed */ - align-items: center; - justify-content: center; -} - -.modal.show { - display: flex; - align-items: center; - justify-content: center; -} - -.modal-content { - background-color: #fefefe; - padding: 30px; - border: 1px solid #888; - width: 80%; /* Could be more or less, depending on screen size */ - max-width: 600px; - border-radius: 15px; - box-shadow: 0 10px 30px rgba(0,0,0,0.2); - position: relative; - animation: slideIn 0.3s ease-out; - margin: auto; -} - -.modal-content h2 { - margin-top: 0; - color: #2c3e50; - border-bottom: none; /* Remove border from h2 inside modal */ - padding-bottom: 0; -} - -.modal-content p { - color: #7f8c8d; - font-size: 14px; - margin-bottom: 15px; -} - -.modal-content textarea { - width: 100%; - padding: 10px; - border: 1px solid #ccc; - border-radius: 8px; - margin-bottom: 20px; - font-family: monospace; /* Use monospace for keys */ - font-size: 14px; - resize: vertical; /* Allow vertical resizing */ - min-height: 150px; -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 15px; - margin-top: 10px; -} - -.close-btn { - color: #aaa; - position: absolute; - top: 10px; - right: 20px; - font-size: 28px; - font-weight: bold; - line-height: 1; -} - -.close-btn:hover, -.close-btn:focus { - color: black; - text-decoration: none; - cursor: pointer; -} - -@keyframes slideIn { - from { transform: translateY(-50px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } -} - - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -@media (max-width: 768px) { - .container { - width: 100%; - padding: 20px; - margin: 10px auto; - } - body { - padding: 10px; - } - h1 { - font-size: 24px; - } - .nav-tabs { - flex-direction: column; - align-items: center; - gap: 10px; - } - .tab-link { - width: 100%; - justify-content: center; - border-radius: 8px; - } - .tab-link.active::after { - display: none; - } - .config-tabs { - flex-direction: column; - } - .tab-btn { - width: 100%; - text-align: center; - } - .toggle { - flex-direction: column; - align-items: flex-start; - gap: 10px; - } - .form-actions { - flex-direction: column; - gap: 15px; - } - .save-btn, .reset-btn { - width: 100%; - justify-content: center; - } - .scroll-buttons { - right: 10px; - bottom: 10px; - } - .scroll-btn { - width: 35px; - height: 35px; - font-size: 16px; - } - .refresh-btn { - top: 10px; - right: 10px; - padding: 8px 16px; - font-size: 12px; - } -} - -@media (max-width: 480px) { - .container { - padding: 15px; - } - h1 { - font-size: 20px; - } - .config-section { - padding: 15px; - } - .config-item input[type="text"], - .config-item input[type="number"], - .config-item select { - padding: 10px; - font-size: 14px; - } -} diff --git a/app/static/css/error_logs.css b/app/static/css/error_logs.css deleted file mode 100644 index 073fdb5..0000000 --- a/app/static/css/error_logs.css +++ /dev/null @@ -1,390 +0,0 @@ -/* error_logs.css - Styles specific to the error logs page, complementing config_editor.css */ - -/* Inherit body, container, h1, nav-tabs, scroll-buttons, refresh-btn, copyright from config_editor.css */ - -/* Add padding to the main content section */ -.config-section { - padding: 25px; /* Increased padding */ - background-color: #fff; /* Ensure white background */ - border-radius: 12px; /* Slightly larger radius */ - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); /* Softer shadow */ - margin-top: 20px; -} -/* Style the controls container */ -.controls-container { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - padding: 15px; - background-color: #f8f9fa; /* Lighter background */ - border-radius: 6px; /* Slightly smaller radius */ - border: 1px solid #e9ecef; /* Lighter border */ - flex-wrap: wrap; /* Allow wrapping on smaller screens */ - gap: 15px; /* Add gap between items */ -} - -.page-size-selector { - display: flex; - align-items: center; - gap: 8px; -} - -.page-size-selector label { - font-weight: bold; - color: #2c3e50; - margin-bottom: 0; /* Override default label margin */ -} - -.page-size-selector select { - padding: 8px 12px; - border: 1px solid rgba(0,0,0,0.1); - border-radius: 6px; - background-color: white; - font-size: 14px; - min-width: 70px; /* Ensure select has some width */ -} - -.page-size-selector span { - color: #2c3e50; -} - -/* Style the action button (refresh) */ -.action-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border: none; - padding: 10px 20px; - border-radius: 25px; - cursor: pointer; - font-size: 14px; - font-weight: bold; - transition: all 0.3s ease; - display: flex; - align-items: center; - gap: 8px; - box-shadow: 0 4px 10px rgba(0,0,0,0.1); -} - -.action-btn:hover { - transform: translateY(-2px); - box-shadow: 0 8px 15px rgba(118, 75, 162, 0.2); -} - -.action-btn i { - font-size: 1em; -} - -/* Search Container Styles */ -.search-container { - display: flex; - flex-wrap: wrap; /* Allow wrapping on smaller screens */ - gap: 10px; /* Space between elements */ - align-items: center; - padding: 15px; - background-color: #f8f9fa; /* Consistent light background */ - border-radius: 6px; - border: 1px solid #e9ecef; /* Lighter border */ - margin-bottom: 25px; /* Increased margin */ -} - -.search-container input[type="text"], -.search-container input[type="date"] { - padding: 8px 12px; - border: 1px solid rgba(0,0,0,0.1); - border-radius: 6px; - font-size: 14px; - flex-grow: 1; /* Allow inputs to grow */ - min-width: 150px; /* Minimum width for inputs */ -} - -.search-container input[type="date"] { - min-width: 130px; /* Slightly less width for date inputs */ - flex-grow: 0; /* Don't let date inputs grow excessively */ -} - - -.search-container span { - color: #2c3e50; - margin: 0 5px; -} - -.search-container .action-btn { - padding: 8px 15px; /* Slightly smaller padding for search button */ - font-size: 14px; - flex-shrink: 0; /* Prevent button from shrinking */ -} - -/* Table container and styled table */ -.table-container { - overflow-x: auto; /* Allow horizontal scrolling for table */ - background-color: #ffffff; - border-radius: 6px; - box-shadow: 0 2px 8px rgba(0,0,0,0.06); /* Softer shadow */ - border: 1px solid #e9ecef; /* Lighter border */ -} - -.styled-table { - width: 100%; - border-collapse: collapse; - font-size: 14px; - color: #333; -} - -.styled-table thead tr { - background-color: #eef2f7; /* Lighter, slightly blueish header */ - text-align: left; - font-weight: 600; /* Slightly bolder */ - color: #495057; /* Darker grey text */ - border-bottom: 1px solid #dee2e6; /* Thinner border */ -} - -.styled-table th, -.styled-table td { - padding: 14px 15px; /* Increased padding */ - border-bottom: 1px solid #f1f3f5; /* Very light border */ - vertical-align: middle; -} - -.styled-table tbody tr { - transition: background-color 0.2s ease; -} - -.styled-table tbody tr:hover { - background-color: #f8f9fa; /* Lighter hover effect */ -} - -/* Zebra striping for table body */ -.styled-table tbody tr:nth-child(even) { - background-color: #fcfdff; /* Very light blue for even rows */ -} - -.styled-table tbody tr:nth-child(even):hover { - background-color: #f0f4f8; /* Slightly darker hover for even rows */ -} - -.styled-table tbody tr:last-of-type { - border-bottom: none; /* Remove border from last row */ -} - -/* Error log content truncation */ -.error-log-content { - max-width: 250px; /* Adjust as needed */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: help; /* Indicate it's expandable/clickable */ -} - -/* Style for the 'View Details' button */ -.btn-view-details { - background-color: #e9ecef; /* Light grey background */ - color: #495057; /* Dark grey text */ - border: 1px solid #dee2e6; /* Subtle border */ - padding: 4px 8px; /* Slightly smaller padding */ - border-radius: 4px; - cursor: pointer; - font-size: 12px; - transition: all 0.2s ease; -} - -.btn-view-details:hover { - background-color: #dee2e6; /* Darker grey on hover */ - border-color: #ced4da; - color: #212529; -} - -/* Status Indicators (Loading, No Data, Error) */ -.status-indicator { - text-align: center; - padding: 30px 15px; - margin: 20px 0; - border-radius: 8px; - display: none; /* Hidden by default */ - color: #555; -} - -.status-indicator.loading { - background-color: rgba(240, 240, 240, 0.7); -} - -.status-indicator.no-data { - background-color: rgba(240, 240, 240, 0.7); -} - -.status-indicator.error { - background-color: rgba(231, 76, 60, 0.1); /* Light red background */ - color: #c0392b; /* Darker red text */ - font-weight: bold; -} - -.status-indicator p { - margin-top: 10px; - margin-bottom: 0; - font-size: 1.1em; -} - -/* Basic spinner animation */ -.spinner { - border: 4px solid rgba(0, 0, 0, 0.1); - width: 36px; - height: 36px; - border-radius: 50%; - border-left-color: #667eea; - animation: spin 1s ease infinite; - margin: 0 auto; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Pagination Styles */ -.pagination-container { - display: flex; - justify-content: center; - padding: 20px 0; - margin-top: 20px; - border-top: 1px solid rgba(0,0,0,0.1); -} - -.pagination { - list-style: none; - padding: 0; - margin: 0; - display: flex; - gap: 5px; -} - -.pagination .page-item { - margin: 0; -} - -.pagination .page-link { - display: block; - padding: 8px 14px; /* Slightly wider */ - color: #6c757d; /* Grey text */ - background-color: white; - border: 1px solid #dee2e6; /* Lighter border */ - border-radius: 4px; - text-decoration: none; - transition: all 0.3s ease; - font-size: 14px; -} - -.pagination .page-link:hover { - background-color: #e9ecef; /* Light grey hover */ - border-color: #dee2e6; -} - -.pagination .page-item.active .page-link { - background: linear-gradient(135deg, #708cf3 0%, #8b60d5 100%); /* Adjusted gradient */ - color: white; - border-color: #708cf3; - font-weight: 600; -} - -.pagination .page-item.disabled .page-link { - color: #aaa; - pointer-events: none; - background-color: #f9f9f9; - border-color: #ddd; -} - -/* Modal Styles (Inherit base from config_editor.css, add specifics) */ -#logDetailModal .modal-content { - max-width: 800px; /* Wider modal for logs */ -} - -#logDetailModal h2 { - font-size: 1.5em; - margin-bottom: 20px; - padding-bottom: 10px; - border-bottom: 1px solid #eee; -} - -.modal-body-content { - max-height: 60vh; /* Limit height and allow scrolling */ - overflow-y: auto; - padding-right: 15px; /* Space for scrollbar */ -} - -.detail-item { - margin-bottom: 15px; -} - -.detail-item h6 { - font-weight: bold; - color: #2c3e50; - margin-bottom: 5px; -} - -.detail-item p, -.detail-item pre { - background-color: #e9ecef; /* Lighter background for code blocks */ - padding: 12px; - border-radius: 5px; - border: 1px solid #dee2e6; /* Match border color */ - font-size: 14px; - color: #333; - margin: 0; - white-space: pre-wrap; /* Ensure pre content wraps */ - word-wrap: break-word; -} - -.detail-item pre { - max-height: 200px; /* Limit height of individual pre blocks */ - overflow-y: auto; -} - -/* Notification Style (Inherited from config_editor.css) */ -#copyStatus { - /* Uses .notification styles from config_editor.css */ - background-color: rgba(39, 174, 96, 0.9); /* Green for success */ -} - -/* Responsive Adjustments */ -@media (max-width: 768px) { - .controls-container { - flex-direction: column; - align-items: stretch; /* Stretch items to full width */ - } - - .page-size-selector { - justify-content: center; /* Center page size selector */ - } - - .action-btn { - width: 100%; /* Full width button */ - justify-content: center; - } - - .styled-table { - font-size: 13px; - } - - .styled-table th, - .styled-table td { - padding: 10px 8px; - } - - .error-log-content { - max-width: 150px; - } - - #logDetailModal .modal-content { - width: 95%; - padding: 20px; - } -} - -@media (max-width: 480px) { - .styled-table { - font-size: 12px; - } - .btn-view-details { - padding: 4px 8px; - font-size: 11px; - } -} diff --git a/app/static/css/keys_status.css b/app/static/css/keys_status.css deleted file mode 100644 index 858cf18..0000000 --- a/app/static/css/keys_status.css +++ /dev/null @@ -1,563 +0,0 @@ -body { - font-family: 'Roboto', sans-serif; - line-height: 1.6; - margin: 0; - padding: 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - min-height: 100vh; -} - -.container { - max-width: 900px; - width: 95%; - background: rgba(255, 255, 255, 0.95); - padding: 40px; - border-radius: 20px; - box-shadow: 0 15px 35px rgba(0,0,0,0.2); - backdrop-filter: blur(10px); - position: relative; - margin: 20px auto; - overflow-y: auto; - max-height: calc(100vh - 40px); - scrollbar-width: none; - -ms-overflow-style: none; -} - -.container::-webkit-scrollbar { - display: none; -} - -h1 { - color: #2c3e50; - text-align: center; - margin-bottom: 15px; - font-weight: 700; - font-size: 32px; - position: relative; - padding-bottom: 15px; -} - -h1::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 100px; - height: 4px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 2px; -} - -/* 导航标签样式 */ -.nav-tabs { - display: flex; - justify-content: center; - margin-bottom: 30px; - border-bottom: 2px solid rgba(0,0,0,0.1); - padding-bottom: 10px; - width: 100%; - max-width: 500px; - margin-left: auto; - margin-right: auto; -} - -.tab-link { - color: #2c3e50; - text-decoration: none; - padding: 12px 25px; - border-radius: 8px 8px 0 0; - font-weight: bold; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - position: relative; - margin: 0 5px; - flex: 1; - text-align: center; -} - -.tab-link:hover { - background: rgba(102, 126, 234, 0.1); -} - -.tab-link.active { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3); -} - -.tab-link.active::after { - content: ''; - position: absolute; - bottom: -12px; - left: 0; - width: 100%; - height: 4px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 2px; -} - -.tab-link i { - font-size: 16px; -} - -.key-list { - margin-bottom: 30px; - background: rgba(248, 249, 250, 0.9); - padding: 25px; - border-radius: 15px; - transition: all 0.3s ease; - border: 1px solid rgba(0,0,0,0.1); - animation: fadeIn 0.5s ease forwards; -} - -.key-list:hover { - transform: translateY(-5px); - box-shadow: 0 10px 20px rgba(0,0,0,0.1); -} - -.key-list:nth-child(2) { - animation-delay: 0.2s; -} - -.key-list h2 { - color: #2c3e50; - margin-bottom: 20px; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 1.5em; - padding-bottom: 10px; - border-bottom: 2px solid rgba(0,0,0,0.1); - cursor: pointer; -} - -.key-list h2 .toggle-icon { - margin-right: 10px; - transition: transform 0.3s ease; -} - -.key-list h2 .toggle-icon.collapsed { - transform: rotate(-90deg); -} - -.key-list .key-content { - transition: all 0.3s ease-out; - overflow: hidden; - height: auto; - opacity: 1; -} - -.key-list .key-content.collapsed { - height: 0; - opacity: 0; - padding-top: 0; - padding-bottom: 0; -} - -ul { - list-style-type: none; - padding: 0; - margin: 0; -} - -li { - background: white; - border: 1px solid rgba(0,0,0,0.1); - margin-bottom: 12px; - padding: 15px; - border-radius: 10px; - transition: all 0.3s ease; - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: 0 2px 5px rgba(0,0,0,0.05); -} - -li:hover { - transform: translateX(5px); - box-shadow: 0 5px 15px rgba(0,0,0,0.1); -} - -.key-info { - display: flex; - align-items: center; - gap: 15px; - flex: 1; /* Allow key-info to take up available space */ - min-width: 0; /* Prevent flex item from overflowing */ -} - -.key-text { - font-family: 'Roboto Mono', monospace; - color: #2c3e50; - word-break: break-all; /* Ensure long keys wrap */ - flex-shrink: 1; /* Allow key text to shrink if needed */ - margin-right: 10px; /* Add space between key text and toggle button */ -} - -.fail-count { - background: rgba(231, 76, 60, 0.1); - color: #e74c3c; - padding: 4px 10px; - border-radius: 15px; - font-size: 0.85em; - display: flex; - align-items: center; - gap: 5px; -} - -.fail-count i { - font-size: 12px; -} - -.key-actions { - display: flex; - gap: 10px; - align-items: center; -} - -.verify-btn, .copy-btn { - color: white; - border: none; - padding: 8px 15px; - border-radius: 30px; - cursor: pointer; - font-size: 14px; - font-weight: bold; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 5px; - box-shadow: 0 4px 10px rgba(0,0,0,0.1); -} - -.verify-btn { - background: linear-gradient(135deg, #2ecc71, #27ae60); -} - -.verify-btn:hover { - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(46, 204, 113, 0.3); -} - -.verify-btn:disabled { - opacity: 0.7; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -.verify-btn i { - font-size: 14px; -} - -.copy-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -} - -.copy-btn:hover { - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3); - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); -} - -.copy-btn:active { - transform: translateY(1px); - box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2); -} - -.copy-btn i { - font-size: 14px; -} - -.total { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 15px 25px; - border-radius: 10px; - font-weight: bold; - text-align: center; - font-size: 1.2em; - margin-top: 30px; - box-shadow: 0 5px 15px rgba(0,0,0,0.1); -} - -#copyStatus { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - padding: 15px 30px; - border-radius: 25px; - font-weight: bold; - opacity: 0; - transition: all 0.3s ease; - backdrop-filter: blur(5px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); - z-index: 1000; - text-align: center; - min-width: 200px; - color: white; -} - -#copyStatus.success { - background: rgba(39, 174, 96, 0.95); -} - -#copyStatus.error { - background: rgba(231, 76, 60, 0.95); -} - -.status-badge { - padding: 4px 12px; - border-radius: 15px; - font-size: 0.9em; - font-weight: bold; - margin-right: 10px; -} - -.status-valid { - background: rgba(39, 174, 96, 0.1); - color: #27ae60; -} - -.status-invalid { - background: rgba(231, 76, 60, 0.1); - color: #e74c3c; -} - -.scroll-buttons { - position: fixed; - right: 20px; - bottom: 20px; - display: none; - flex-direction: column; - gap: 10px; - z-index: 1000; -} - -.scroll-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - width: 40px; - height: 40px; - border: none; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; - transition: all 0.3s ease; - backdrop-filter: blur(5px); - box-shadow: 0 2px 10px rgba(0,0,0,0.2); -} - -.scroll-btn:hover { - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); - transform: scale(1.1); -} - -.scroll-btn:active { - transform: scale(0.95); -} - -.refresh-btn { - position: fixed; - top: 20px; - right: 20px; - z-index: 1000; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: #fff; - border: none; - padding: 10px 20px; - border-radius: 25px; - cursor: pointer; - font-size: 14px; - font-weight: bold; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); -} - -.refresh-btn:hover { - transform: scale(1.05); - box-shadow: 0 8px 20px rgba(118, 75, 162, 0.3); - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); -} - -.refresh-btn:active { - transform: scale(0.95); -} - -.refresh-btn i { - transition: transform 0.5s ease; -} - -.refresh-btn.loading i { - animation: spin 1s linear infinite; -} - -.copyright { - position: fixed; - bottom: 0; - left: 0; - width: 100%; - background: rgba(255, 255, 255, 0.9); - padding: 10px 0; - text-align: center; - font-size: 14px; - color: #2c3e50; - backdrop-filter: blur(5px); - border-top: 1px solid rgba(0,0,0,0.1); -} - -.copyright a { - color: #764ba2; - text-decoration: none; - transition: color 0.3s ease; -} - -.copyright a:hover { - color: #667eea; -} - -.copyright img { - width: 20px; - height: 20px; - border-radius: 50%; - vertical-align: middle; - margin-right: 5px; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -/* Toggle Visibility Button Styles */ -.toggle-vis-btn { - background: none; - border: none; - color: #7f8c8d; /* Subtle color */ - cursor: pointer; - padding: 5px; - font-size: 16px; - transition: color 0.3s ease; - margin-left: 5px; /* Space from key text */ - flex-shrink: 0; /* Prevent button from shrinking */ -} - -.toggle-vis-btn:hover { - color: #34495e; /* Darker color on hover */ -} - -.toggle-vis-btn i { - vertical-align: middle; -} - -@media (max-width: 768px) { - .container { - width: 100%; - padding: 20px; - margin: 10px auto; - } - body { - padding: 10px; - } - h1 { - font-size: 24px; - } - .nav-tabs { - flex-direction: column; - align-items: center; - gap: 10px; - } - .tab-link { - width: 100%; - justify-content: center; - border-radius: 8px; - } - .tab-link.active::after { - display: none; - } - .key-list h2 { - font-size: 1.2em; - flex-direction: column; - gap: 10px; - align-items: flex-start; - } - .key-info { - flex-direction: column; - align-items: flex-start; - gap: 8px; - width: 100%; /* Ensure key-info takes full width */ - } - li { - flex-direction: column; - gap: 10px; - } - .key-actions { - width: 100%; - flex-direction: column; - } - - .verify-btn, .copy-btn { - width: 100%; - justify-content: center; - } - .key-text { - /* word-break: break-all; */ /* Already applied above */ - margin-right: 0; /* Remove right margin on smaller screens */ - } - .scroll-buttons { - right: 10px; - bottom: 10px; - } - .scroll-btn { - width: 35px; - height: 35px; - font-size: 16px; - } - .refresh-btn { - top: 10px; - right: 10px; - padding: 8px 16px; - font-size: 12px; - } -} - -@media (max-width: 480px) { - .container { - padding: 15px; - } - h1 { - font-size: 20px; - } - .key-list { - padding: 15px; - } - .status-badge { - padding: 3px 8px; - font-size: 0.8em; - } - .fail-count { - font-size: 0.8em; - } - .total { - font-size: 1em; - padding: 12px 20px; - } -} diff --git a/app/static/icons/logo.png b/app/static/icons/logo.png new file mode 100644 index 0000000..3c2831b Binary files /dev/null and b/app/static/icons/logo.png differ diff --git a/app/static/icons/logo1.png b/app/static/icons/logo1.png new file mode 100644 index 0000000..685e962 Binary files /dev/null and b/app/static/icons/logo1.png differ diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index 850db49..0513062 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -310,9 +310,13 @@ function switchTab(tabId) { const tabButtons = document.querySelectorAll('.tab-btn'); tabButtons.forEach(button => { if (button.getAttribute('data-tab') === tabId) { - button.classList.add('active'); + // 激活状态:主色背景,白色文字,添加阴影 + button.classList.remove('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70'); + button.classList.add('bg-primary-600', 'text-white', 'shadow-md'); } else { - button.classList.remove('active'); + // 非激活状态:白色背景,灰色文字,无阴影 + button.classList.remove('bg-primary-600', 'text-white', 'shadow-md'); + button.classList.add('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70'); } }); @@ -354,18 +358,19 @@ function addArrayItemWithValue(key, value) { if (!container) return; const arrayItem = document.createElement('div'); - arrayItem.className = 'array-item'; + arrayItem.className = 'array-item flex justify-between items-center mb-2'; // 使用 Flexbox 布局,垂直居中,底部增加间距 const input = document.createElement('input'); input.type = 'text'; input.name = `${key}[]`; input.value = value; - input.className = 'array-input'; + input.className = 'array-input flex-grow px-3 py-2 rounded-md border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 mr-2'; // 输入框占据大部分空间,添加样式和右边距 const removeBtn = document.createElement('button'); removeBtn.type = 'button'; - removeBtn.className = 'remove-btn'; - removeBtn.innerHTML = ''; + removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150 ml-2'; // 新的 Tailwind 样式 + removeBtn.innerHTML = ''; // 改用垃圾桶图标 + removeBtn.title = '删除'; // 添加悬停提示 removeBtn.addEventListener('click', function() { arrayItem.remove(); }); @@ -411,12 +416,43 @@ function collectFormData() { return formData; } +// 辅助函数:停止定时任务 +async function stopScheduler() { + try { + const response = await fetch('/api/scheduler/stop', { method: 'POST' }); + if (!response.ok) { + console.warn(`停止定时任务失败: ${response.status}`); + } else { + console.log('定时任务已停止'); + } + } catch (error) { + console.error('调用停止定时任务API时出错:', error); + } +} + +// 辅助函数:启动定时任务 +async function startScheduler() { + try { + const response = await fetch('/api/scheduler/start', { method: 'POST' }); + if (!response.ok) { + console.warn(`启动定时任务失败: ${response.status}`); + } else { + console.log('定时任务已启动'); + } + } catch (error) { + console.error('调用启动定时任务API时出错:', error); + } +} + // 保存配置 async function saveConfig() { try { const formData = collectFormData(); - + showNotification('正在保存配置...', 'info'); + + // 1. 停止定时任务 + await stopScheduler(); const response = await fetch('/api/config', { method: 'PUT', @@ -435,24 +471,37 @@ async function saveConfig() { // 显示保存状态 const saveStatus = document.getElementById('saveStatus'); - saveStatus.classList.add('show'); + saveStatus.style.opacity = "1"; + saveStatus.style.transform = "translate(-50%, -50%) scale(1.1)"; setTimeout(() => { - saveStatus.classList.remove('show'); + saveStatus.style.opacity = "0"; + saveStatus.style.transform = "translate(-50%, -50%) scale(0.95)"; }, 3000); showNotification('配置保存成功', 'success'); + + // 3. 启动新的定时任务 + await startScheduler(); + } catch (error) { console.error('保存配置失败:', error); - + // 保存失败时,也尝试重启定时任务,以防万一 + await startScheduler(); // 显示错误状态 const saveStatus = document.getElementById('saveStatus'); - saveStatus.classList.add('show', 'error'); + saveStatus.style.backgroundColor = "#ef4444"; // 红色背景 + saveStatus.style.opacity = "1"; + saveStatus.style.transform = "translate(-50%, -50%) scale(1.1)"; saveStatus.querySelector('.status-icon i').className = 'fas fa-times-circle'; saveStatus.querySelector('.status-text').textContent = '配置保存失败'; setTimeout(() => { - saveStatus.classList.remove('show', 'error'); + saveStatus.style.opacity = "0"; + saveStatus.style.transform = "translate(-50%, -50%) scale(0.95)"; + setTimeout(() => { + saveStatus.style.backgroundColor = "#22c55e"; // 恢复绿色背景 + }, 300); }, 3000); showNotification('保存配置失败: ' + error.message, 'error'); @@ -491,6 +540,9 @@ function resetConfig(event) { async function executeReset() { try { showNotification('正在重置配置...', 'info'); + + // 1. 停止定时任务 + await stopScheduler(); const response = await fetch('/api/config/reset', { method: 'POST' }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -498,24 +550,48 @@ async function executeReset() { const config = await response.json(); populateForm(config); showNotification('配置已重置为默认值', 'success'); + + // 3. 启动新的定时任务 + await startScheduler(); + } catch (error) { console.error('重置配置失败:', error); showNotification('重置配置失败: ' + error.message, 'error'); + // 重置失败时,也尝试重启定时任务 + await startScheduler(); } } - // 显示通知 function showNotification(message, type = 'info') { const notification = document.getElementById('notification'); notification.textContent = message; - notification.className = 'notification show'; - if (type) { - notification.classList.add(type); + // 设置适当的样式 + if (type === 'error') { + notification.classList.add('bg-danger-500'); + notification.classList.remove('bg-black'); + } else { + notification.classList.remove('bg-danger-500'); + notification.classList.add('bg-black'); + + // 可以为不同类型设置不同的颜色 + if (type === 'success') { + notification.style.backgroundColor = '#22c55e'; // 绿色 + } else if (type === 'info') { + notification.style.backgroundColor = '#3b82f6'; // 蓝色 + } else if (type === 'warning') { + notification.style.backgroundColor = '#f59e0b'; // 橙色 + } } + // 应用过渡效果 - 与keys_status.js中一致 + notification.style.opacity = "1"; + notification.style.transform = "translate(-50%, 0)"; + + // 设置自动消失 setTimeout(() => { - notification.classList.remove('show'); + notification.style.opacity = "0"; + notification.style.transform = "translate(-50%, 10px)"; }, 3000); } @@ -527,8 +603,7 @@ function refreshPage(button) { // 滚动到顶部 function scrollToTop() { - const container = document.querySelector('.container'); - container.scrollTo({ + window.scrollTo({ top: 0, behavior: 'smooth' }); @@ -536,9 +611,8 @@ function scrollToTop() { // 滚动到底部 function scrollToBottom() { - const container = document.querySelector('.container'); - container.scrollTo({ - top: container.scrollHeight, + window.scrollTo({ + top: document.body.scrollHeight, behavior: 'smooth' }); } diff --git a/app/static/js/error_logs.js b/app/static/js/error_logs.js index bea68a6..98a4894 100644 --- a/app/static/js/error_logs.js +++ b/app/static/js/error_logs.js @@ -9,32 +9,12 @@ function scrollToBottom() { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); } -// 刷新页面功能 -function refreshPage(button) { - if (button) { - // Use 'loading' class consistent with config_editor.css animation - button.classList.add('loading'); - // Disable button while refreshing - button.disabled = true; - } - - // Fetch new data instead of full reload for a smoother experience - loadErrorLogs().finally(() => { - if (button) { - // Remove loading class and re-enable button after fetch completes - button.classList.remove('loading'); - button.disabled = false; - } - }); - // Optional: Keep reload as fallback or if preferred - // setTimeout(() => { - // window.location.reload(); - // }, 500); -} +// Refresh function removed as the buttons are gone. +// If refresh functionality is needed elsewhere, it can be triggered directly by calling loadErrorLogs(). // 全局变量 let currentPage = 1; -let pageSize = 20; +let pageSize = 10; // let totalPages = 1; // totalPages will be calculated dynamically based on API response if available, or based on fetched data length let errorLogs = []; // Store fetched logs for details view let currentSearch = { // Store current search parameters @@ -46,7 +26,7 @@ let currentSearch = { // Store current search parameters // DOM Elements Cache let pageSizeSelector; -let refreshBtn; +// let refreshBtn; // Removed, as the button is deleted let tableBody; let paginationElement; let loadingIndicator; @@ -59,12 +39,14 @@ let errorSearchInput; let startDateInput; let endDateInput; let searchBtn; +let pageInput; // 新增:页码输入框 +let goToPageBtn; // 新增:跳转按钮 // 页面加载完成后执行 document.addEventListener('DOMContentLoaded', function() { // Cache DOM elements pageSizeSelector = document.getElementById('pageSize'); - refreshBtn = document.getElementById('refreshBtn'); + // refreshBtn = document.getElementById('refreshBtn'); // Removed tableBody = document.getElementById('errorLogsTable'); paginationElement = document.getElementById('pagination'); loadingIndicator = document.getElementById('loadingIndicator'); @@ -78,6 +60,8 @@ document.addEventListener('DOMContentLoaded', function() { startDateInput = document.getElementById('startDate'); endDateInput = document.getElementById('endDate'); searchBtn = document.getElementById('searchBtn'); + pageInput = document.getElementById('pageInput'); // 新增 + goToPageBtn = document.getElementById('goToPageBtn'); // 新增 // Initialize page size selector if (pageSizeSelector) { @@ -89,18 +73,7 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // Initialize refresh button (using the one inside the controls container) - if (refreshBtn) { - refreshBtn.addEventListener('click', function() { - // Add loading state to the button itself - this.classList.add('loading'); - this.disabled = true; - loadErrorLogs().finally(() => { - this.classList.remove('loading'); - this.disabled = false; - }); - }); - } + // Refresh button event listener removed // Initialize search button if (searchBtn) { @@ -130,8 +103,113 @@ document.addEventListener('DOMContentLoaded', function() { // Initial load of error logs loadErrorLogs(); + + // Add event listeners for copy buttons inside the modal + setupCopyButtons(); + + // 新增:为页码跳转按钮添加事件监听器 + if (goToPageBtn && pageInput) { + goToPageBtn.addEventListener('click', function() { + const targetPage = parseInt(pageInput.value); + // 需要获取总页数来验证输入 + // 暂时无法直接获取 totalPages,需要在 updatePagination 中存储或重新计算 + // 简单的验证:必须是正整数 + if (!isNaN(targetPage) && targetPage >= 1) { + // 理想情况下,应检查 targetPage <= totalPages + // 但 totalPages 可能未知,所以暂时只跳转 + currentPage = targetPage; + loadErrorLogs(); + pageInput.value = ''; // 清空输入框 + } else { + showNotification('请输入有效的页码', 'error', 2000); + pageInput.value = ''; // 清空无效输入 + } + }); + // 允许按 Enter 键跳转 + pageInput.addEventListener('keypress', function(event) { + if (event.key === 'Enter') { + goToPageBtn.click(); // 触发按钮点击 + } + }); + } }); +// Fallback copy function using document.execCommand +function fallbackCopyTextToClipboard(text) { + const textArea = document.createElement("textarea"); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.top = "0"; + textArea.style.left = "0"; + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + let successful = false; + try { + successful = document.execCommand('copy'); + } catch (err) { + console.error('Fallback copy failed:', err); + successful = false; + } + + document.body.removeChild(textArea); + return successful; +} + +// Helper function to handle feedback after copy attempt (both modern and fallback) +function handleCopyResult(buttonElement, success) { + const originalIcon = buttonElement.querySelector('i').className; // Store original icon class + const iconElement = buttonElement.querySelector('i'); + if (success) { + iconElement.className = 'fas fa-check text-success-500'; // Use checkmark icon class + showNotification('已复制到剪贴板', 'success', 2000); + } else { + iconElement.className = 'fas fa-times text-danger-500'; // Use error icon class + showNotification('复制失败', 'error', 3000); + } + setTimeout(() => { iconElement.className = originalIcon; }, success ? 2000 : 3000); // Restore original icon class +} + +// Function to set up copy button listeners (using modern API with fallback) +function setupCopyButtons() { + const copyButtons = document.querySelectorAll('.copy-btn'); + copyButtons.forEach(button => { + button.addEventListener('click', function() { + const targetId = this.getAttribute('data-target'); + const targetElement = document.getElementById(targetId); + + if (targetElement) { + const textToCopy = targetElement.textContent; + let copySuccess = false; + + // Try modern clipboard API first (requires HTTPS or localhost) + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(textToCopy).then(() => { + handleCopyResult(this, true); // Use helper for feedback + }).catch(err => { + console.error('Clipboard API failed, attempting fallback:', err); + // Attempt fallback if modern API fails + copySuccess = fallbackCopyTextToClipboard(textToCopy); + handleCopyResult(this, copySuccess); // Use helper for feedback + }); + } else { + // Use fallback if modern API is not available or context is insecure + console.warn("Clipboard API not available or context insecure. Using fallback copy method."); + copySuccess = fallbackCopyTextToClipboard(textToCopy); + handleCopyResult(this, copySuccess); // Use helper for feedback + } + } else { + console.error('Target element not found:', targetId); + showNotification('复制出错:找不到目标元素', 'error'); + } + }); + }); +} + // 加载错误日志数据 async function loadErrorLogs() { showLoading(true); @@ -389,10 +467,18 @@ function updatePagination(currentItemCount, totalItems) { // Helper function to add pagination links function addPaginationLink(parentElement, text, enabled, clickHandler, isActive = false) { const pageItem = document.createElement('li'); - pageItem.className = `page-item ${!enabled ? 'disabled' : ''} ${isActive ? 'active' : ''}`; + // 移除 'page-item' 和 'active' 类,使用 Tailwind 类进行样式化 + // pageItem.className = `page-item ${!enabled ? 'disabled' : ''} ${isActive ? 'active' : ''}`; const pageLink = document.createElement('a'); - pageLink.className = 'page-link'; + // 使用 Tailwind 类进行样式化 + pageLink.className = `px-3 py-1 rounded-md text-sm transition duration-150 ease-in-out ${ + isActive + ? 'bg-primary-600 text-white font-semibold shadow-md cursor-default' // 突出当前页样式 + : enabled + ? 'bg-white text-gray-700 hover:bg-primary-50 hover:text-primary-600 border border-gray-300' // 可点击页码样式 + : 'bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200' // 禁用状态样式 (如 '...') + }`; pageLink.href = '#'; // Prevent page jump pageLink.innerHTML = text; @@ -402,12 +488,14 @@ function addPaginationLink(parentElement, text, enabled, clickHandler, isActive clickHandler(); }); } else if (!enabled) { - pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on disabled + pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on disabled or active + } else if (isActive) { + pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on active page } - - pageItem.appendChild(pageLink); - parentElement.appendChild(pageItem); + // 不再需要 li 元素,直接将 a 元素添加到父元素 + // pageItem.appendChild(pageLink); + parentElement.appendChild(pageLink); } diff --git a/app/static/js/keys_status.js b/app/static/js/keys_status.js index 413bd02..412d5ac 100644 --- a/app/static/js/keys_status.js +++ b/app/static/js/keys_status.js @@ -27,15 +27,21 @@ function copyToClipboard(text) { function copyKeys(type) { const keys = Array.from(document.querySelectorAll(`#${type}Keys .key-text`)).map(span => span.dataset.fullKey); - const jsonKeys = JSON.stringify(keys); - copyToClipboard(jsonKeys) + if (keys.length === 0) { + showCopyStatus('没有可复制的密钥', true); + return; + } + + const keysText = keys.join('\n'); + + copyToClipboard(keysText) .then(() => { - showCopyStatus(`已成功复制${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`); + showCopyStatus(`已成功复制${keys.length}个${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`); }) .catch((err) => { console.error('无法复制文本: ', err); - showCopyStatus('复制失败,请重试'); + showCopyStatus('复制失败,请重试', true); }); } @@ -46,21 +52,32 @@ function copyKey(key) { }) .catch((err) => { console.error('无法复制文本: ', err); - showCopyStatus('复制失败,请重试'); + showCopyStatus('复制失败,请重试', true); }); } -function showCopyStatus(message, type = 'success') { +function showCopyStatus(message, isError = false) { const statusElement = document.getElementById('copyStatus'); statusElement.textContent = message; - statusElement.className = type; // 设置样式类 - statusElement.style.opacity = 1; + + // 添加适当的样式类 + if (isError) { + statusElement.classList.add('bg-danger-500'); + statusElement.classList.remove('bg-black'); + } else { + statusElement.classList.remove('bg-danger-500'); + statusElement.classList.add('bg-black'); + } + + // 应用过渡效果 + statusElement.style.opacity = "1"; + statusElement.style.transform = "translate(-50%, 0)"; + + // 设置自动消失 setTimeout(() => { - statusElement.style.opacity = 0; - setTimeout(() => { - statusElement.className = ''; // 清除样式类 - }, 300); - }, 2000); + statusElement.style.opacity = "0"; + statusElement.style.transform = "translate(-50%, 10px)"; + }, 3000); } async function verifyKey(key, button) { @@ -70,59 +87,223 @@ async function verifyKey(key, button) { const originalHtml = button.innerHTML; button.innerHTML = ' 验证中'; - const response = await fetch(`/gemini/v1beta/verify-key/${key}`, { + try { + const response = await fetch(`/gemini/v1beta/verify-key/${key}`, { + method: 'POST' + }); + const data = await response.json(); + + // 根据验证结果更新UI并显示模态提示框 + if (data.success || data.status === 'valid') { + // 验证成功,显示成功结果 + button.style.backgroundColor = '#27ae60'; + // 使用结果模态框显示成功消息 + showResultModal(true, '密钥验证成功'); + // 模态框关闭时会自动刷新页面 + } else { + // 验证失败,显示失败结果 + const errorMsg = data.error || '密钥无效'; + button.style.backgroundColor = '#e74c3c'; + // 使用结果模态框显示失败消息,但不自动刷新页面 + showResultModal(false, '密钥验证失败: ' + errorMsg, true); // 改为true以在关闭时刷新 + } + } catch (fetchError) { + console.error('API请求失败:', fetchError); + showResultModal(false, '验证请求失败: ' + fetchError.message, true); // 改为true以在关闭时刷新 + } finally { + // 1秒后恢复按钮原始状态 + setTimeout(() => { + button.innerHTML = originalHtml; + button.disabled = false; + button.style.backgroundColor = ''; + }, 1000); + } + } catch (error) { + console.error('验证失败:', error); + button.disabled = false; + button.innerHTML = ' 验证'; + showResultModal(false, '验证处理失败: ' + error.message, true); // 改为true以在关闭时刷新 + } +} + +async function resetKeyFailCount(key, button) { + try { + // 禁用按钮并显示加载状态 + button.disabled = true; + const originalHtml = button.innerHTML; + button.innerHTML = ' 重置中'; + + const response = await fetch(`/gemini/v1beta/reset-fail-count/${key}`, { method: 'POST' }); const data = await response.json(); - // 根据验证结果更新UI - if (data.status === 'valid') { - showCopyStatus('密钥验证成功', 'success'); + // 根据重置结果更新UI + if (data.success) { + showCopyStatus('失败计数重置成功'); button.style.backgroundColor = '#27ae60'; + setTimeout(() => location.reload(), 1500); } else { - showCopyStatus('密钥验证失败', 'error'); + const errorMsg = data.message || '重置失败'; + showCopyStatus('重置失败: ' + errorMsg, true); button.style.backgroundColor = '#e74c3c'; } - // 3秒后恢复按钮原始状态 + // 1秒后恢复按钮原始状态 setTimeout(() => { button.innerHTML = originalHtml; button.disabled = false; button.style.backgroundColor = ''; - }, 3000); + }, 1000); } catch (error) { - console.error('验证失败:', error); - showCopyStatus('验证请求失败', 'error'); + console.error('重置失败:', error); + showCopyStatus('重置请求失败: ' + error.message, true); button.disabled = false; - button.innerHTML = ' 验证'; + button.innerHTML = ' 重置'; + } +} + +function showResetModal(type) { + const modalElement = document.getElementById('resetModal'); + const titleElement = document.getElementById('resetModalTitle'); + const messageElement = document.getElementById('resetModalMessage'); + const confirmButton = document.getElementById('confirmResetBtn'); + + // 设置标题和消息 + titleElement.textContent = '批量重置失败次数'; + messageElement.textContent = `确定要批量重置${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`; + + // 设置确认按钮事件 + confirmButton.onclick = () => executeResetAll(type); + + // 显示模态框 + modalElement.classList.remove('hidden'); +} + +function closeResetModal() { + document.getElementById('resetModal').classList.add('hidden'); +} + +// 触发显示模态框 +function resetAllKeysFailCount(type, event) { + // 阻止事件冒泡 + if (event) { + event.stopPropagation(); + } + + // 显示模态确认框 + showResetModal(type); +} + +// 执行批量重置 +// 关闭模态框并根据参数决定是否刷新页面 +function closeResultModal(reload = true) { + document.getElementById('resultModal').classList.add('hidden'); + if (reload) { + location.reload(); // 操作完成后刷新页面 + } +} + +// 显示操作结果模态框 +function showResultModal(success, message, autoReload = true) { + const modalElement = document.getElementById('resultModal'); + const titleElement = document.getElementById('resultModalTitle'); + const messageElement = document.getElementById('resultModalMessage'); + const iconElement = document.getElementById('resultIcon'); + const confirmButton = document.getElementById('resultModalConfirmBtn'); + + // 设置标题 + titleElement.textContent = success ? '操作成功' : '操作失败'; + + // 设置图标 + if (success) { + iconElement.innerHTML = ''; + iconElement.className = 'text-5xl mb-3 text-success-500'; + } else { + iconElement.innerHTML = ''; + iconElement.className = 'text-5xl mb-3 text-danger-500'; + } + + // 设置消息 + messageElement.textContent = message; + + // 设置确认按钮点击事件 + confirmButton.onclick = () => closeResultModal(autoReload); + + // 显示模态框 + modalElement.classList.remove('hidden'); +} + +async function executeResetAll(type) { + try { + // 关闭确认模态框 + closeResetModal(); + + // 使用data-reset-type属性直接找到对应的重置按钮 + const resetButton = document.querySelector(`button[data-reset-type="${type}"]`); + + if (!resetButton) { + // 如果找不到按钮,显示错误并返回 + showResultModal(false, `找不到${type === 'valid' ? '有效' : '无效'}密钥区域的批量重置按钮`); + return; + } + + // 禁用按钮并显示加载状态 + resetButton.disabled = true; + const originalHtml = resetButton.innerHTML; + resetButton.innerHTML = ' 重置中'; + + try { + // 调用API,传递类型参数 + const response = await fetch(`/gemini/v1beta/reset-all-fail-counts?key_type=${type}`, { + method: 'POST' + }); + + if (!response.ok) { + throw new Error(`服务器返回错误: ${response.status}`); + } + + const data = await response.json(); + + // 根据重置结果显示模态框 + if (data.success) { + const message = data.reset_count ? + `成功重置${data.reset_count}个${type === 'valid' ? '有效' : '无效'}密钥的失败次数` : + '所有失败次数重置成功'; + showResultModal(true, message); + } else { + const errorMsg = data.message || '批量重置失败'; + showResultModal(false, '批量重置失败: ' + errorMsg); + } + } catch (fetchError) { + console.error('API请求失败:', fetchError); + showResultModal(false, '批量重置请求失败: ' + fetchError.message); + } finally { + // 恢复按钮原始状态 + setTimeout(() => { + resetButton.innerHTML = originalHtml; + resetButton.disabled = false; + }, 500); + } + } catch (error) { + console.error('批量重置失败:', error); + showResultModal(false, '批量重置处理失败: ' + error.message); } } function scrollToTop() { - const container = document.querySelector('.container'); - container.scrollTo({ - top: 0, - behavior: 'smooth' - }); + window.scrollTo({ top: 0, behavior: 'smooth' }); } function scrollToBottom() { - const container = document.querySelector('.container'); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); } +// 移除这个函数,因为它可能正在干扰按钮的显示 +// HTML中已经设置了滚动按钮为flex显示,不需要JavaScript额外控制 function updateScrollButtons() { - const container = document.querySelector('.container'); - const scrollButtons = document.querySelector('.scroll-buttons'); - if (container.scrollHeight > container.clientHeight) { - scrollButtons.style.display = 'flex'; - } else { - scrollButtons.style.display = 'none'; - } + // 不执行任何操作 } function refreshPage(button) { @@ -142,22 +323,50 @@ function toggleSection(header, sectionId) { content.classList.toggle('collapsed'); } +// 筛选有效密钥(根据失败次数阈值) +function filterValidKeys() { + const thresholdInput = document.getElementById('failCountThreshold'); + const validKeyItems = document.querySelectorAll('#validKeys li'); + // 读取阈值,如果输入无效或为空,则默认为0(不过滤) + const threshold = parseInt(thresholdInput.value, 10); + const filterThreshold = isNaN(threshold) || threshold < 0 ? 0 : threshold; + + validKeyItems.forEach(item => { + const failCount = parseInt(item.dataset.failCount, 10); + // 如果失败次数大于等于阈值,则显示,否则隐藏 + if (failCount >= filterThreshold) { + item.style.display = ''; // 显示 + } else { + item.style.display = 'none'; // 隐藏 + } + }); +} + // 初始化 document.addEventListener('DOMContentLoaded', () => { - // 检查滚动按钮 - updateScrollButtons(); - + // 移除对滚动按钮显示的控制,让它们由HTML/CSS控制 + // 监听展开/折叠事件 document.querySelectorAll('.key-list h2').forEach(header => { header.addEventListener('click', () => { - setTimeout(updateScrollButtons, 300); + // 不再调用updateScrollButtons }); }); // 更新版权年份 - const copyrightYear = document.querySelector('.copyright script'); - if (copyrightYear) { - copyrightYear.textContent = new Date().getFullYear(); + const copyrightYearElement = document.querySelector('.copyright script'); + if (copyrightYearElement && copyrightYearElement.parentNode.classList.contains('copyright')) { + // 确保只更新版权部分的年份 + copyrightYearElement.textContent = new Date().getFullYear(); + } + + // 添加筛选输入框事件监听 + const thresholdInput = document.getElementById('failCountThreshold'); + if (thresholdInput) { + // 使用 'input' 事件实时响应输入变化 + thresholdInput.addEventListener('input', filterValidKeys); + // 初始加载时应用一次筛选(基于默认值1) + filterValidKeys(); } }); @@ -174,8 +383,8 @@ if ('serviceWorker' in navigator) { }); } function toggleKeyVisibility(button) { - const keyInfoDiv = button.closest('.key-info'); - const keyTextSpan = keyInfoDiv.querySelector('.key-text'); + const keyContainer = button.closest('.flex.items-center.gap-1'); + const keyTextSpan = keyContainer.querySelector('.key-text'); const eyeIcon = button.querySelector('i'); const fullKey = keyTextSpan.dataset.fullKey; const maskedKey = fullKey.substring(0, 4) + '...' + fullKey.substring(fullKey.length - 4); diff --git a/app/templates/auth.html b/app/templates/auth.html index 0945654..9ebdcff 100644 --- a/app/templates/auth.html +++ b/app/templates/auth.html @@ -1,42 +1,124 @@ - - -
- - -