- New web UI with OpenWebUI-like interface using Tailwind CSS - SQLite-based authentication with session management - User registration, login, and profile pages - Chat interface with conversation history - Streaming responses with visible thinking phase - File attachments support - User document uploads in profile - Rate limiting (5 requests/day for free users) - Admin panel user management with promote/demote/delete - Custom color theme matching balloon logo design - Compatible with Nuitka build system
344 lines
10 KiB
Python
Executable File
344 lines
10 KiB
Python
Executable File
"""
|
|
Hidden Admin UI Routes
|
|
Configuration, Document Upload, and ComfyUI Workflow Management
|
|
"""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Request, UploadFile, File, Form, HTTPException
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from pydantic import BaseModel
|
|
from loguru import logger
|
|
|
|
from config import settings, get_data_dir, get_workflows_dir, save_config_to_db, load_config_from_db
|
|
from auth.models import get_auth_manager
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
# Templates
|
|
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "admin" / "templates")
|
|
|
|
|
|
# ============================================================================
|
|
# Config Models
|
|
# ============================================================================
|
|
|
|
class EndpointConfig(BaseModel):
|
|
"""API endpoint configuration."""
|
|
gemini_api_key: Optional[str] = None
|
|
gemini_model: str = "gemini-1.5-flash"
|
|
openrouter_api_key: Optional[str] = None
|
|
openrouter_model: str = "meta-llama/llama-3-8b-instruct:free"
|
|
comfyui_host: str = "http://127.0.0.1:8188"
|
|
ollama_host: str = "http://127.0.0.1:11434"
|
|
ollama_model: str = "qwen2.5:2b"
|
|
embedding_model: str = "qwen3-embedding:4b"
|
|
|
|
|
|
# ============================================================================
|
|
# Admin UI Routes
|
|
# ============================================================================
|
|
|
|
@router.get("/", response_class=HTMLResponse)
|
|
async def admin_dashboard(request: Request):
|
|
"""Admin dashboard homepage."""
|
|
config = load_config_from_db()
|
|
return templates.TemplateResponse(
|
|
"dashboard.html",
|
|
{
|
|
"request": request,
|
|
"config": config,
|
|
"settings": settings
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/endpoints", response_class=HTMLResponse)
|
|
async def endpoints_page(request: Request):
|
|
"""API endpoint configuration page."""
|
|
config = load_config_from_db()
|
|
return templates.TemplateResponse(
|
|
"endpoints.html",
|
|
{
|
|
"request": request,
|
|
"config": config,
|
|
"settings": settings
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/endpoints")
|
|
async def save_endpoints(config: EndpointConfig):
|
|
"""Save endpoint configuration to database."""
|
|
config_dict = config.model_dump(exclude_none=True)
|
|
for key, value in config_dict.items():
|
|
save_config_to_db(key, value)
|
|
|
|
logger.info("Endpoint configuration saved")
|
|
return {"status": "success", "message": "Configuration saved"}
|
|
|
|
|
|
@router.get("/documents", response_class=HTMLResponse)
|
|
async def documents_page(request: Request):
|
|
"""Document management page."""
|
|
rag_store = request.app.state.rag_store
|
|
|
|
documents = rag_store.list_documents()
|
|
return templates.TemplateResponse(
|
|
"documents.html",
|
|
{
|
|
"request": request,
|
|
"documents": documents,
|
|
"settings": settings
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/documents/upload")
|
|
async def upload_document(
|
|
request: Request,
|
|
file: UploadFile = File(...),
|
|
chunk_size: int = Form(default=500),
|
|
overlap: int = Form(default=50)
|
|
):
|
|
"""Upload and index a document."""
|
|
rag_store = request.app.state.rag_store
|
|
|
|
# Read file content
|
|
content = await file.read()
|
|
|
|
# Process based on file type
|
|
filename = file.filename or "unknown"
|
|
file_ext = Path(filename).suffix.lower()
|
|
|
|
try:
|
|
doc_id = await rag_store.add_document(
|
|
filename=filename,
|
|
content=content,
|
|
file_type=file_ext,
|
|
chunk_size=chunk_size,
|
|
overlap=overlap
|
|
)
|
|
|
|
logger.info(f"Document uploaded: {filename} (ID: {doc_id})")
|
|
return {"status": "success", "document_id": doc_id, "filename": filename}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to upload document: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/documents/{doc_id}")
|
|
async def delete_document(doc_id: str, request: Request):
|
|
"""Delete a document from the store."""
|
|
rag_store = request.app.state.rag_store
|
|
|
|
try:
|
|
rag_store.delete_document(doc_id)
|
|
logger.info(f"Document deleted: {doc_id}")
|
|
return {"status": "success"}
|
|
except Exception as e:
|
|
logger.error(f"Failed to delete document: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/comfyui", response_class=HTMLResponse)
|
|
async def comfyui_page(request: Request):
|
|
"""ComfyUI workflow management page."""
|
|
config = load_config_from_db()
|
|
workflows_dir = get_workflows_dir()
|
|
|
|
workflows = {
|
|
"image": None,
|
|
"video": None,
|
|
"audio": None
|
|
}
|
|
|
|
for workflow_type in workflows.keys():
|
|
workflow_path = workflows_dir / f"{workflow_type}.json"
|
|
if workflow_path.exists():
|
|
with open(workflow_path, "r") as f:
|
|
workflows[workflow_type] = json.load(f)
|
|
|
|
return templates.TemplateResponse(
|
|
"comfyui.html",
|
|
{
|
|
"request": request,
|
|
"config": config,
|
|
"workflows": workflows,
|
|
"workflows_dir": str(workflows_dir),
|
|
"settings": settings
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/comfyui/upload")
|
|
async def upload_comfyui_workflow(
|
|
workflow_type: str = Form(...),
|
|
file: UploadFile = File(...)
|
|
):
|
|
"""Upload a ComfyUI workflow JSON file."""
|
|
if workflow_type not in ["image", "video", "audio"]:
|
|
raise HTTPException(status_code=400, detail="Invalid workflow type")
|
|
|
|
workflows_dir = get_workflows_dir()
|
|
workflow_path = workflows_dir / f"{workflow_type}.json"
|
|
|
|
try:
|
|
content = await file.read()
|
|
# Validate JSON
|
|
workflow_data = json.loads(content)
|
|
|
|
with open(workflow_path, "wb") as f:
|
|
f.write(content)
|
|
|
|
logger.info(f"ComfyUI workflow uploaded: {workflow_type}")
|
|
return {"status": "success", "workflow_type": workflow_type}
|
|
|
|
except json.JSONDecodeError:
|
|
raise HTTPException(status_code=400, detail="Invalid JSON file")
|
|
except Exception as e:
|
|
logger.error(f"Failed to upload workflow: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/comfyui/{workflow_type}")
|
|
async def get_comfyui_workflow(workflow_type: str):
|
|
"""Get a ComfyUI workflow JSON."""
|
|
if workflow_type not in ["image", "video", "audio"]:
|
|
raise HTTPException(status_code=400, detail="Invalid workflow type")
|
|
|
|
workflows_dir = get_workflows_dir()
|
|
workflow_path = workflows_dir / f"{workflow_type}.json"
|
|
|
|
if not workflow_path.exists():
|
|
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
|
|
with open(workflow_path, "r") as f:
|
|
return json.load(f)
|
|
|
|
|
|
@router.delete("/comfyui/{workflow_type}")
|
|
async def delete_comfyui_workflow(workflow_type: str):
|
|
"""Delete a ComfyUI workflow."""
|
|
if workflow_type not in ["image", "video", "audio"]:
|
|
raise HTTPException(status_code=400, detail="Invalid workflow type")
|
|
|
|
workflows_dir = get_workflows_dir()
|
|
workflow_path = workflows_dir / f"{workflow_type}.json"
|
|
|
|
if workflow_path.exists():
|
|
workflow_path.unlink()
|
|
logger.info(f"ComfyUI workflow deleted: {workflow_type}")
|
|
|
|
return {"status": "success"}
|
|
|
|
|
|
# ============================================================================
|
|
# User Management Routes
|
|
# ============================================================================
|
|
|
|
@router.get("/users", response_class=HTMLResponse)
|
|
async def users_page(request: Request):
|
|
"""User management page."""
|
|
auth = get_auth_manager()
|
|
users = auth.get_all_users()
|
|
|
|
return templates.TemplateResponse(
|
|
"users.html",
|
|
{
|
|
"request": request,
|
|
"users": users,
|
|
"settings": settings
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/users/{user_id}/promote")
|
|
async def promote_user(user_id: str):
|
|
"""Promote a user to admin."""
|
|
auth = get_auth_manager()
|
|
success = auth.promote_to_admin(user_id)
|
|
|
|
if success:
|
|
logger.info(f"User promoted to admin: {user_id}")
|
|
return {"status": "success"}
|
|
|
|
raise HTTPException(status_code=400, detail="Cannot promote this user")
|
|
|
|
|
|
@router.post("/users/{user_id}/demote")
|
|
async def demote_user(user_id: str):
|
|
"""Demote a user from admin."""
|
|
auth = get_auth_manager()
|
|
success = auth.demote_from_admin(user_id)
|
|
|
|
if success:
|
|
logger.info(f"User demoted from admin: {user_id}")
|
|
return {"status": "success"}
|
|
|
|
raise HTTPException(status_code=400, detail="Cannot demote this user")
|
|
|
|
|
|
@router.post("/users/{user_id}/limit")
|
|
async def update_user_limit(user_id: str, limit: int = Form(...)):
|
|
"""Update a user's daily request limit."""
|
|
auth = get_auth_manager()
|
|
success = auth.update_user_limit(user_id, limit)
|
|
|
|
if success:
|
|
logger.info(f"User limit updated: {user_id} -> {limit}")
|
|
return {"status": "success"}
|
|
|
|
raise HTTPException(status_code=400, detail="Failed to update user limit")
|
|
|
|
|
|
@router.delete("/users/{user_id}")
|
|
async def delete_user(user_id: str):
|
|
"""Delete a user."""
|
|
auth = get_auth_manager()
|
|
success = auth.delete_user(user_id)
|
|
|
|
if success:
|
|
logger.info(f"User deleted: {user_id}")
|
|
return {"status": "success"}
|
|
|
|
raise HTTPException(status_code=400, detail="Cannot delete this user")
|
|
|
|
|
|
@router.get("/status")
|
|
async def get_status(request: Request):
|
|
"""Get system status."""
|
|
rag_store = request.app.state.rag_store
|
|
config = load_config_from_db()
|
|
|
|
# Check Ollama connectivity
|
|
ollama_status = "unknown"
|
|
try:
|
|
import httpx
|
|
async with httpx.AsyncClient() as client:
|
|
resp = await client.get(f"{config.get('ollama_host', settings.ollama_host)}/api/tags", timeout=5.0)
|
|
ollama_status = "connected" if resp.status_code == 200 else "error"
|
|
except Exception:
|
|
ollama_status = "disconnected"
|
|
|
|
# Check ComfyUI connectivity
|
|
comfyui_status = "unknown"
|
|
try:
|
|
import httpx
|
|
async with httpx.AsyncClient() as client:
|
|
resp = await client.get(f"{config.get('comfyui_host', settings.comfyui_host)}/system_stats", timeout=5.0)
|
|
comfyui_status = "connected" if resp.status_code == 200 else "error"
|
|
except Exception:
|
|
comfyui_status = "disconnected"
|
|
|
|
return {
|
|
"ollama": ollama_status,
|
|
"comfyui": comfyui_status,
|
|
"documents_count": rag_store.get_document_count(),
|
|
"chunks_count": rag_store.get_chunk_count(),
|
|
}
|