ge-tool/backend/routes/tms_routes.py

212 lines
8.9 KiB
Python
Raw Normal View History

2025-12-10 06:41:43 +00:00
"""
TMS Permission Management Routes
Handles submission creation, listing, deletion, retry, and queue display.
"""
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from typing import List
import logging
from ..services import supabase_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["TMS"])
# ==================== REQUEST MODELS ====================
class SubmissionCreate(BaseModel):
submission_id: str
usernames: List[str]
ge_input: str
# ==================== SUBMISSION ENDPOINTS ====================
@router.post("/submissions")
def create_submission(payload: SubmissionCreate):
try:
created = supabase_service.create_submission_supabase(
payload.submission_id,
payload.usernames,
payload.ge_input
)
return created
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/submissions")
def list_submissions(limit: int = 50):
submissions = supabase_service.get_submissions_supabase(limit=limit)
return {"success": True, "submissions": submissions}
@router.get("/submissions/{submission_id}")
def get_submission(submission_id: str):
sub_list = supabase_service.get_submissions_supabase(limit=100)
sub = next((item for item in sub_list if item.get("submission_id") == submission_id), None)
if not sub:
raise HTTPException(status_code=404, detail="Submission not found")
return {"success": True, "submission": sub}
@router.delete("/submissions/{submission_id}")
def delete_submission(submission_id: str):
ok = supabase_service.delete_submission_supabase(submission_id)
if not ok:
raise HTTPException(status_code=404, detail="Submission not found or could not be deleted")
return {"success": True}
@router.post("/submissions/{submission_id}/retry")
def retry_submission(submission_id: str, payload: dict = None): # type: ignore
"""Retry a submission with only the error GE IDs and error usernames.
If payload.errorGeIds and payload.errorUsernames are provided, create a new submission
with only those GE IDs and usernames that had errors.
Otherwise, reset the original submission to pending (legacy behavior).
"""
if payload and payload.get('errorGeIds') and payload.get('errorUsernames'):
error_ge_ids = payload['errorGeIds']
error_usernames = payload['errorUsernames']
# Create new submission with only error GE IDs and error usernames
new_ge_id_and_lang = '\n'.join(error_ge_ids)
username_str = ','.join(error_usernames)
created = supabase_service.create_retry_submission(username_str, new_ge_id_and_lang)
if not created:
raise HTTPException(status_code=500, detail="Failed to create retry submission")
return {"success": True, "newSubmissionId": created.get("id")}
else:
# Legacy behavior: reset status to pending
ok = supabase_service.update_submission_supabase(submission_id, status="pending")
if not ok:
raise HTTPException(status_code=404, detail="Submission not found or could not be updated")
return {"success": True}
# ==================== DRIVER MANAGEMENT ====================
@router.post('/driver/close')
def close_driver(request: Request):
"""Close the global Selenium WebDriver if it's running.
Security policy:
- If environment variable DRIVER_ADMIN_TOKEN is set, require header X-Admin-Token matching it.
- If DRIVER_ADMIN_TOKEN is not set, only allow requests from localhost (127.0.0.1 or ::1).
"""
import os
try:
admin_token = os.environ.get('DRIVER_ADMIN_TOKEN')
header_token = request.headers.get('x-admin-token')
client_host = request.client.host if request.client else ''
if admin_token:
if not header_token or header_token != admin_token:
raise HTTPException(status_code=401, detail='Invalid or missing admin token')
else:
# allow only requests originating from localhost when no token configured
if client_host not in ('127.0.0.1', '::1', 'localhost'):
raise HTTPException(status_code=403, detail='Driver close is restricted to localhost')
# TMS permission automation is handled by TypeScript backend now
# This endpoint is kept for backward compatibility but does nothing
return {'success': True, 'message': 'Driver management moved to TypeScript backend'}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ==================== USERNAME MANAGEMENT ====================
@router.get('/usernames')
def get_usernames():
usernames = supabase_service.get_userslist()
return {"success": True, "usernames": usernames}
@router.get('/usernames/search')
def search_usernames(q: str = ""):
"""Search usernames by query string (case-insensitive, contains match)."""
all_usernames = supabase_service.get_userslist()
if not q:
return {"success": True, "suggestions": all_usernames[:20]}
q_lower = q.lower()
suggestions = [u for u in all_usernames if q_lower in u.lower()]
return {"success": True, "suggestions": suggestions[:20]}
@router.post('/usernames')
def add_username(payload: dict):
new_username = payload.get('username') if isinstance(payload, dict) else None
if not new_username:
raise HTTPException(status_code=400, detail='username is required')
return supabase_service.add_username(new_username)
@router.delete('/usernames')
def delete_username(payload: dict):
username = payload.get('username') if isinstance(payload, dict) else None
if not username:
raise HTTPException(status_code=400, detail='username is required')
return supabase_service.delete_username(username)
# ==================== QUEUE DISPLAY ====================
@router.get('/queue')
def get_queue(limit: int = 100, all: bool = False):
"""Return a flattened list of GE items built from pending submissions in Supabase.
Each pending submission's `input.ge_input` (newline separated) is split into GE ID and lang
and turned into an item consumable by the frontend `QueueStatus` component.
"""
try:
# By default include only pending and processing submissions so UI can show the single processing submission
# If caller passes all=true, include completed and failed as well (useful when a single endpoint should provide history)
all_subs = supabase_service.get_submissions_supabase(limit=1000) or []
allowed = ('pending', 'processing') if not all else ('pending', 'processing', 'completed', 'failed')
subs = [d for d in all_subs if str(d.get('status', '')).lower() in allowed]
items = []
for doc in subs:
submission_id = doc.get('submission_id')
usernames = doc.get('input', {}).get('usernames', []) if isinstance(doc.get('input'), dict) else []
usernames_str = '\n'.join(usernames) if isinstance(usernames, list) else (usernames or '')
ge_input = doc.get('input', {}).get('ge_input', '') if isinstance(doc.get('input'), dict) else ''
# split lines and create items
lines = [l.strip() for l in str(ge_input).splitlines() if l and l.strip()]
for idx, line in enumerate(lines):
parts = line.split() # expect e.g. "1000 de" or "696 us"
ge_id = parts[0] if len(parts) > 0 else line
lang = parts[1] if len(parts) > 1 else ''
key = f"{submission_id}:{idx}"
raw_status = str(doc.get('status', 'pending')).lower()
# map backend status to frontend status labels
if raw_status == 'pending':
mapped_status = 'waiting'
elif raw_status == 'processing':
mapped_status = 'processing'
elif raw_status == 'completed':
# when requesting all, represent completed as done
mapped_status = 'done'
elif raw_status == 'failed':
mapped_status = 'error'
else:
mapped_status = raw_status
items.append({
'key': key,
'id': str(ge_id),
'lang': str(lang),
'status': mapped_status,
'usernames': usernames_str,
'submission_id': submission_id
})
# respect limit on resulting GE items
return {'success': True, 'queue': items[:limit]}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))