mirror of
https://github.com/trailofbits/buttercup
synced 2026-06-21 14:11:39 +00:00
fix: web UI error handling (#289)
* Add failed tasks counter and error reporting * Remove failed tasks tab * Simplify changes * Fix pov ui error * Fix PoV dashboard tab. Fix lint. * Update tests
This commit is contained in:
@@ -56,7 +56,7 @@ _settings: Settings | None = None
|
||||
_database_manager: DatabaseManager | None = None
|
||||
|
||||
# Compatibility - keeping dashboard_stats as backup/cache
|
||||
dashboard_stats = {"activeTasks": 0, "totalPovs": 0, "totalPatches": 0, "totalBundles": 0}
|
||||
dashboard_stats = {"activeTasks": 0, "totalPovs": 0, "totalPatches": 0, "totalBundles": 0, "failedTasks": 0}
|
||||
|
||||
|
||||
# Dashboard models
|
||||
@@ -77,12 +77,19 @@ class TaskInfo(BaseModel):
|
||||
bundles: List[Dict[str, Any]] = []
|
||||
created_at: str
|
||||
|
||||
# CRS submission status and error information
|
||||
crs_submission_status: Optional[str] = None # 'pending', 'success', 'failed'
|
||||
crs_submission_error: Optional[str] = None
|
||||
crs_error_details: Optional[str] = None # JSON string
|
||||
crs_submission_timestamp: Optional[str] = None
|
||||
|
||||
|
||||
class DashboardStats(BaseModel):
|
||||
activeTasks: int
|
||||
totalPovs: int
|
||||
totalPatches: int
|
||||
totalBundles: int
|
||||
failedTasks: int
|
||||
|
||||
|
||||
class Challenge(BaseModel):
|
||||
@@ -291,12 +298,23 @@ if static_dir.exists():
|
||||
|
||||
|
||||
# Utility functions for task management
|
||||
def calculate_task_status(deadline: datetime) -> str:
|
||||
"""Calculate task status based on deadline."""
|
||||
def calculate_task_status(task: Task) -> str:
|
||||
"""Calculate task status based on deadline and CRS submission status."""
|
||||
try:
|
||||
now = datetime.now(deadline.tzinfo)
|
||||
# First check if the task failed to submit to CRS
|
||||
crs_status = getattr(task, "crs_submission_status", None)
|
||||
if crs_status == "failed":
|
||||
return "failed"
|
||||
|
||||
# Then check deadline
|
||||
deadline = getattr(task, "deadline", None)
|
||||
if deadline is None:
|
||||
return "active" # Default to active if no deadline
|
||||
|
||||
now = datetime.now(deadline.tzinfo) if deadline.tzinfo else datetime.now()
|
||||
if now > deadline:
|
||||
return "expired"
|
||||
|
||||
return "active"
|
||||
except Exception:
|
||||
return "active" # Default to active if parsing fails
|
||||
@@ -310,10 +328,14 @@ def update_dashboard_stats(database_manager: DatabaseManager) -> None:
|
||||
total_povs = 0
|
||||
total_patches = 0
|
||||
total_bundles = 0
|
||||
failed_tasks_count = 0
|
||||
with database_manager.get_all_tasks() as tasks:
|
||||
for task in tasks:
|
||||
if calculate_task_status(task.deadline) == "active":
|
||||
task_status = calculate_task_status(task)
|
||||
if task_status == "active":
|
||||
active_count += 1
|
||||
if task_status == "failed":
|
||||
failed_tasks_count += 1
|
||||
|
||||
total_povs += len(task.povs)
|
||||
total_patches += len(task.patches)
|
||||
@@ -325,76 +347,104 @@ def update_dashboard_stats(database_manager: DatabaseManager) -> None:
|
||||
"totalPovs": total_povs,
|
||||
"totalPatches": total_patches,
|
||||
"totalBundles": total_bundles,
|
||||
"failedTasks": failed_tasks_count,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def pov_to_pov_info(pov: POV) -> dict[str, Any]:
|
||||
return {
|
||||
"pov_id": pov.pov_id,
|
||||
"timestamp": pov.created_at,
|
||||
"architecture": pov.architecture,
|
||||
"engine": pov.engine,
|
||||
"fuzzer_name": pov.fuzzer_name,
|
||||
"sanitizer": pov.sanitizer,
|
||||
"testcase": base64.b64encode(pov.testcase),
|
||||
}
|
||||
try:
|
||||
return {
|
||||
"pov_id": getattr(pov, "pov_id", "unknown"),
|
||||
"timestamp": getattr(pov, "created_at", datetime.now()),
|
||||
"architecture": getattr(pov, "architecture", "unknown"),
|
||||
"engine": getattr(pov, "engine", "unknown"),
|
||||
"fuzzer_name": getattr(pov, "fuzzer_name", "unknown"),
|
||||
"sanitizer": getattr(pov, "sanitizer", "unknown"),
|
||||
"testcase": base64.b64encode(getattr(pov, "testcase", b"")),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting POV to info: {e}")
|
||||
return {"pov_id": "error", "error": str(e)}
|
||||
|
||||
|
||||
def patch_to_patch_info(patch: Patch) -> dict[str, Any]:
|
||||
return {
|
||||
"patch_id": patch.patch_id,
|
||||
"timestamp": patch.created_at,
|
||||
"patch": base64.b64encode(patch.patch.encode("utf-8", errors="ignore")),
|
||||
}
|
||||
try:
|
||||
return {
|
||||
"patch_id": getattr(patch, "patch_id", "unknown"),
|
||||
"timestamp": getattr(patch, "created_at", datetime.now()),
|
||||
"patch": base64.b64encode(getattr(patch, "patch", "").encode("utf-8", errors="ignore")),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting patch to info: {e}")
|
||||
return {"patch_id": "error", "error": str(e)}
|
||||
|
||||
|
||||
def bundle_to_bundle_info(bundle: Bundle) -> dict[str, Any]:
|
||||
return {
|
||||
"bundle_id": bundle.bundle_id,
|
||||
"timestamp": bundle.created_at,
|
||||
"description": bundle.description,
|
||||
"broadcast_sarif_id": bundle.broadcast_sarif_id,
|
||||
"freeform_id": bundle.freeform_id,
|
||||
"patch_id": bundle.patch_id,
|
||||
"pov_id": bundle.pov_id,
|
||||
"submitted_sarif_id": bundle.submitted_sarif_id,
|
||||
}
|
||||
try:
|
||||
return {
|
||||
"bundle_id": getattr(bundle, "bundle_id", "unknown"),
|
||||
"timestamp": getattr(bundle, "created_at", datetime.now()),
|
||||
"description": getattr(bundle, "description", ""),
|
||||
"broadcast_sarif_id": getattr(bundle, "broadcast_sarif_id", None),
|
||||
"freeform_id": getattr(bundle, "freeform_id", None),
|
||||
"patch_id": getattr(bundle, "patch_id", None),
|
||||
"pov_id": getattr(bundle, "pov_id", None),
|
||||
"submitted_sarif_id": getattr(bundle, "submitted_sarif_id", None),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting bundle to info: {e}")
|
||||
return {"bundle_id": "error", "error": str(e)}
|
||||
|
||||
|
||||
def task_to_task_info(task: Task) -> TaskInfo:
|
||||
"""Convert a task to a TaskInfo object."""
|
||||
povs = []
|
||||
for pov in task.povs:
|
||||
povs.append(pov_to_pov_info(pov))
|
||||
try:
|
||||
povs = []
|
||||
for pov in task.povs:
|
||||
povs.append(pov_to_pov_info(pov))
|
||||
|
||||
patches = []
|
||||
for patch in task.patches:
|
||||
patches.append(patch_to_patch_info(patch))
|
||||
patches = []
|
||||
for patch in task.patches:
|
||||
patches.append(patch_to_patch_info(patch))
|
||||
|
||||
bundles = []
|
||||
for bundle in task.bundles:
|
||||
bundles.append(bundle_to_bundle_info(bundle))
|
||||
bundles = []
|
||||
for bundle in task.bundles:
|
||||
bundles.append(bundle_to_bundle_info(bundle))
|
||||
|
||||
task_data = {
|
||||
"task_id": task.task_id,
|
||||
"name": task.name,
|
||||
"project_name": task.project_name,
|
||||
"status": calculate_task_status(task.deadline),
|
||||
"duration": task.duration,
|
||||
"deadline": datetime.isoformat(task.deadline),
|
||||
"challenge_repo_url": task.challenge_repo_url,
|
||||
"challenge_repo_head_ref": task.challenge_repo_head_ref,
|
||||
"challenge_repo_base_ref": task.challenge_repo_base_ref,
|
||||
"fuzz_tooling_url": task.fuzz_tooling_url,
|
||||
"fuzz_tooling_ref": task.fuzz_tooling_ref,
|
||||
"created_at": datetime.isoformat(task.created_at),
|
||||
"povs": povs,
|
||||
"patches": patches,
|
||||
"bundles": bundles,
|
||||
}
|
||||
task_data = {
|
||||
"task_id": task.task_id,
|
||||
"name": getattr(task, "name", None),
|
||||
"project_name": getattr(task, "project_name", "Unknown"),
|
||||
"status": calculate_task_status(task),
|
||||
"duration": getattr(task, "duration", 0),
|
||||
"deadline": getattr(task, "deadline", datetime.now()).isoformat()
|
||||
if getattr(task, "deadline", None)
|
||||
else None,
|
||||
"challenge_repo_url": getattr(task, "challenge_repo_url", None),
|
||||
"challenge_repo_head_ref": getattr(task, "challenge_repo_head_ref", None),
|
||||
"challenge_repo_base_ref": getattr(task, "challenge_repo_base_ref", None),
|
||||
"fuzz_tooling_url": getattr(task, "fuzz_tooling_url", None),
|
||||
"fuzz_tooling_ref": getattr(task, "fuzz_tooling_ref", None),
|
||||
"created_at": getattr(task, "created_at", datetime.now()).isoformat()
|
||||
if getattr(task, "created_at", None)
|
||||
else None,
|
||||
"povs": povs,
|
||||
"patches": patches,
|
||||
"bundles": bundles,
|
||||
# CRS submission status and error information
|
||||
"crs_submission_status": getattr(task, "crs_submission_status", None),
|
||||
"crs_submission_error": getattr(task, "crs_submission_error", None),
|
||||
"crs_error_details": getattr(task, "crs_error_details", None),
|
||||
"crs_submission_timestamp": getattr(task, "crs_submission_timestamp", datetime.fromtimestamp(0)).isoformat()
|
||||
if getattr(task, "crs_submission_timestamp", None)
|
||||
else None,
|
||||
}
|
||||
|
||||
return TaskInfo(**task_data)
|
||||
return TaskInfo(**task_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in task_to_task_info for task {getattr(task, 'task_id', 'unknown')}: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def _create_task(
|
||||
@@ -418,6 +468,8 @@ def _create_task(
|
||||
now = datetime.now()
|
||||
deadline = now + timedelta(seconds=challenge.duration)
|
||||
|
||||
# Always create the task in the database first
|
||||
logger.info(f"Creating task {task_id} in database for challenge {challenge.name}")
|
||||
database_manager.create_task(
|
||||
task_id=task_id,
|
||||
name=name,
|
||||
@@ -431,18 +483,104 @@ def _create_task(
|
||||
fuzz_tooling_url=challenge.fuzz_tooling_url,
|
||||
fuzz_tooling_ref=challenge.fuzz_tooling_ref,
|
||||
)
|
||||
logger.info(f"Task {task_id} created successfully in database")
|
||||
|
||||
# Send task to CRS via POST /v1/task endpoint
|
||||
if crs_client.submit_task(task):
|
||||
logger.info(f"Submitting task {task_id} to CRS...")
|
||||
crs_response = crs_client.submit_task(task)
|
||||
|
||||
# Update the task with CRS submission results
|
||||
if crs_response.success:
|
||||
logger.info(f"Task {task_id} submitted successfully to CRS")
|
||||
return Message(message=f"Task {task_id} created and submitted to CRS for challenge {challenge.name}")
|
||||
database_manager.update_task_crs_status(task_id=task_id, crs_submission_status="success")
|
||||
logger.info(f"Task {task_id} CRS status updated to 'success'")
|
||||
return Message(
|
||||
message=f"Task {task_id} created and submitted successfully to CRS for challenge {challenge.name}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Failed to submit task {task_id} to CRS")
|
||||
return Error(message="Failed to submit task to CRS")
|
||||
|
||||
# Generate user-friendly error message
|
||||
error_message = crs_response.get_user_friendly_error_message("Failed to submit task to CRS")
|
||||
logger.error(f"Task {task_id} error message: {error_message}")
|
||||
|
||||
# Update the task with error information
|
||||
logger.info(f"Updating task {task_id} CRS status to 'failed' with error details")
|
||||
database_manager.update_task_crs_status(
|
||||
task_id=task_id,
|
||||
crs_submission_status="failed",
|
||||
crs_submission_error=error_message,
|
||||
crs_error_details=crs_response.error_details,
|
||||
)
|
||||
logger.info(f"Task {task_id} CRS status updated to 'failed' with error details")
|
||||
|
||||
# Verify the task was updated correctly
|
||||
with database_manager.get_task(task_id) as updated_task:
|
||||
if updated_task:
|
||||
logger.info(f"Verified task {task_id} in database:")
|
||||
logger.info(f" - CRS status: {getattr(updated_task, 'crs_submission_status', 'N/A')}")
|
||||
logger.info(f" - CRS error: {getattr(updated_task, 'crs_submission_error', 'N/A')}")
|
||||
logger.info(f" - CRS error details: {getattr(updated_task, 'crs_error_details', 'N/A')}")
|
||||
else:
|
||||
logger.error(f"Task {task_id} not found in database after update!")
|
||||
|
||||
# Return success message since the task was created, but include the CRS error
|
||||
return Message(
|
||||
message=f"Task {task_id} created but failed to submit to CRS. "
|
||||
f"Task is visible in the dashboard with error details. "
|
||||
f"Error: {error_message}",
|
||||
color="error",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating task for challenge {challenge.name}: {e}")
|
||||
return Error(message=f"Failed to create task: {str(e)}")
|
||||
|
||||
# Create a failed task in the database even when the challenge service fails
|
||||
try:
|
||||
# Generate a unique task ID for the failed task
|
||||
failed_task_id = str(uuid.uuid4())
|
||||
name = challenge.name or f"failed-{failed_task_id[:8]}"
|
||||
now = datetime.now()
|
||||
deadline = now + timedelta(seconds=challenge.duration)
|
||||
|
||||
logger.info(f"Creating failed task {failed_task_id} in database for challenge {challenge.name}")
|
||||
database_manager.create_task(
|
||||
task_id=failed_task_id,
|
||||
name=name,
|
||||
project_name=challenge.fuzz_tooling_project_name,
|
||||
status="failed",
|
||||
duration=challenge.duration,
|
||||
deadline=deadline,
|
||||
challenge_repo_url=challenge.challenge_repo_url,
|
||||
challenge_repo_head_ref=challenge.challenge_repo_head_ref,
|
||||
challenge_repo_base_ref=challenge.challenge_repo_base_ref,
|
||||
fuzz_tooling_url=challenge.fuzz_tooling_url,
|
||||
fuzz_tooling_ref=challenge.fuzz_tooling_ref,
|
||||
)
|
||||
|
||||
# Mark it as failed with the error details
|
||||
error_message = f"Failed to create task: {str(e)}"
|
||||
database_manager.update_task_crs_status(
|
||||
task_id=failed_task_id,
|
||||
crs_submission_status="failed",
|
||||
crs_submission_error=error_message,
|
||||
crs_error_details={"exception": str(e), "error_type": "challenge_service_failure"},
|
||||
)
|
||||
|
||||
logger.info(f"Failed task {failed_task_id} created in database with error details")
|
||||
|
||||
# Return a message indicating the task was created but failed
|
||||
return Message(
|
||||
message=f"Task {failed_task_id} created but failed during setup. "
|
||||
f"Task is visible in the dashboard with error details. "
|
||||
f"Error: {error_message}",
|
||||
color="error",
|
||||
)
|
||||
|
||||
except Exception as db_error:
|
||||
logger.error(f"Failed to create failed task in database: {db_error}")
|
||||
# If we can't even create the failed task, return the original error
|
||||
return Error(message=f"Failed to create task: {str(e)}")
|
||||
|
||||
|
||||
def _create_sarif_broadcast(
|
||||
@@ -460,10 +598,22 @@ def _create_sarif_broadcast(
|
||||
|
||||
sarif = body["sarif"]
|
||||
broadcast = challenge_service.create_sarif_broadcast(task_id, sarif)
|
||||
if crs_client.submit_sarif_broadcast(broadcast):
|
||||
|
||||
crs_response = crs_client.submit_sarif_broadcast(broadcast)
|
||||
|
||||
if crs_response.success:
|
||||
return Message(message=f"SARIF Broadcast for Task {task_id} created and submitted to CRS")
|
||||
else:
|
||||
return Error(message=f"Failed to submit SARIF Broadcast for Task {task_id} to CRS")
|
||||
# Use the helper method to generate user-friendly error message
|
||||
error_message = crs_response.get_user_friendly_error_message(
|
||||
f"Failed to submit SARIF Broadcast for Task {task_id} to CRS"
|
||||
)
|
||||
|
||||
# Return a message with the error details for better user experience
|
||||
return Message(
|
||||
message=f"SARIF Broadcast for Task {task_id} created but failed to submit to CRS. Error: {error_message}",
|
||||
color="error",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/v1/ping/", response_model=PingResponse, tags=["ping"])
|
||||
@@ -536,6 +686,74 @@ def get_dashboard_tasks(database_manager: DatabaseManager = Depends(get_database
|
||||
return tasks_list
|
||||
|
||||
|
||||
@app.get("/v1/dashboard/tasks/failed", response_model=List[TaskInfo], tags=["dashboard"])
|
||||
def get_failed_tasks(database_manager: DatabaseManager = Depends(get_database_manager)) -> List[TaskInfo]:
|
||||
"""
|
||||
Get list of failed tasks for debugging
|
||||
"""
|
||||
logger.info("get_failed_tasks endpoint called")
|
||||
|
||||
try:
|
||||
failed_tasks = database_manager.get_tasks_by_crs_status("failed")
|
||||
logger.info(f"Found {len(failed_tasks)} failed tasks in database")
|
||||
|
||||
if failed_tasks:
|
||||
logger.info(f"First failed task ID: {failed_tasks[0].task_id}")
|
||||
logger.info(f"First failed task CRS status: {getattr(failed_tasks[0], 'crs_submission_status', 'N/A')}")
|
||||
logger.info(f"First failed task CRS error: {getattr(failed_tasks[0], 'crs_submission_error', 'N/A')}")
|
||||
|
||||
tasks_list = []
|
||||
for task in failed_tasks:
|
||||
try:
|
||||
task_info = task_to_task_info(task)
|
||||
tasks_list.append(task_info)
|
||||
logger.info(f"Converted task {task.task_id} to TaskInfo with status: {task_info.status}")
|
||||
except Exception as task_error:
|
||||
logger.error(f"Error converting task {task.task_id} to TaskInfo: {task_error}", exc_info=True)
|
||||
# Skip this task and continue with others
|
||||
continue
|
||||
|
||||
# Sort by created_at descending (newest first)
|
||||
tasks_list.sort(key=lambda x: x.created_at, reverse=True)
|
||||
logger.info(f"Returning {len(tasks_list)} failed tasks")
|
||||
|
||||
return tasks_list
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_failed_tasks: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@app.get("/v1/dashboard/tasks/active", response_model=List[TaskInfo], tags=["dashboard"])
|
||||
def get_active_tasks(database_manager: DatabaseManager = Depends(get_database_manager)) -> List[TaskInfo]:
|
||||
"""
|
||||
Get list of active tasks
|
||||
"""
|
||||
active_tasks = database_manager.get_tasks_by_status("active")
|
||||
tasks_list = []
|
||||
for task in active_tasks:
|
||||
tasks_list.append(task_to_task_info(task))
|
||||
|
||||
# Sort by created_at descending (newest first)
|
||||
tasks_list.sort(key=lambda x: x.created_at, reverse=True)
|
||||
return tasks_list
|
||||
|
||||
|
||||
@app.get("/v1/dashboard/tasks/expired", response_model=List[TaskInfo], tags=["dashboard"])
|
||||
def get_expired_tasks(database_manager: DatabaseManager = Depends(get_database_manager)) -> List[TaskInfo]:
|
||||
"""
|
||||
Get list of expired tasks
|
||||
"""
|
||||
expired_tasks = database_manager.get_tasks_by_status("expired")
|
||||
tasks_list = []
|
||||
for task in expired_tasks:
|
||||
tasks_list.append(task_to_task_info(task))
|
||||
|
||||
# Sort by created_at descending (newest first)
|
||||
tasks_list.sort(key=lambda x: x.created_at, reverse=True)
|
||||
return tasks_list
|
||||
|
||||
|
||||
@app.get("/v1/dashboard/tasks/{task_id}", response_model=TaskInfo, tags=["dashboard"])
|
||||
def get_dashboard_task(task_id: str, database_manager: DatabaseManager = Depends(get_database_manager)) -> TaskInfo:
|
||||
"""
|
||||
@@ -548,6 +766,36 @@ def get_dashboard_task(task_id: str, database_manager: DatabaseManager = Depends
|
||||
return task_to_task_info(task)
|
||||
|
||||
|
||||
@app.get("/v1/dashboard/tasks/{task_id}/crs-status", tags=["dashboard"])
|
||||
def get_task_crs_status(task_id: str, database_manager: DatabaseManager = Depends(get_database_manager)) -> dict:
|
||||
"""
|
||||
Get detailed CRS submission status and error information for a specific task
|
||||
"""
|
||||
with database_manager.get_task(task_id) as task:
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
import json
|
||||
|
||||
# Parse error details if present
|
||||
error_details = None
|
||||
if hasattr(task, "crs_error_details") and task.crs_error_details:
|
||||
try:
|
||||
error_details = json.loads(task.crs_error_details)
|
||||
except json.JSONDecodeError:
|
||||
error_details = {"raw": task.crs_error_details}
|
||||
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"crs_submission_status": getattr(task, "crs_submission_status", None),
|
||||
"crs_submission_error": getattr(task, "crs_submission_error", None),
|
||||
"crs_error_details": error_details,
|
||||
"crs_submission_timestamp": getattr(task, "crs_submission_timestamp", datetime.fromtimestamp(0)).isoformat()
|
||||
if getattr(task, "crs_submission_timestamp", None)
|
||||
else None,
|
||||
}
|
||||
|
||||
|
||||
@app.get(
|
||||
"/v1/request/list/",
|
||||
response_model=RequestListResponse,
|
||||
|
||||
@@ -49,7 +49,8 @@ class FuzzingEngine(Enum):
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
message: Optional[str] = None
|
||||
message: str
|
||||
color: Optional[str] = "default" # "default", "success", "warning", "error"
|
||||
|
||||
|
||||
class POVSubmission(BaseModel):
|
||||
|
||||
+192
-19
@@ -2,19 +2,190 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import requests
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from buttercup.orchestrator.ui.competition_api.models.crs_types import Task, SARIFBroadcast
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CRSResponse:
|
||||
"""Response from CRS API calls with detailed status and error information."""
|
||||
|
||||
def __init__(
|
||||
self, success: bool, status_code: int, response_text: str = "", error_details: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
self.success = success
|
||||
self.status_code = status_code
|
||||
self.response_text = response_text
|
||||
self.error_details = error_details or {}
|
||||
|
||||
def get_user_friendly_error_message(self, base_message: str = "Operation failed") -> str:
|
||||
"""
|
||||
Generate a user-friendly error message from the response details.
|
||||
|
||||
Args:
|
||||
base_message: Base message to start with
|
||||
|
||||
Returns:
|
||||
Formatted error message suitable for display to users
|
||||
"""
|
||||
if self.success:
|
||||
return ""
|
||||
|
||||
error_message = base_message
|
||||
|
||||
# Add specific error information
|
||||
if self.error_details.get("error_message"):
|
||||
error_message += f": {self.error_details['error_message']}"
|
||||
|
||||
# Add validation errors if present
|
||||
validation_errors = self.error_details.get("validation_errors", {})
|
||||
if validation_errors:
|
||||
if isinstance(validation_errors, dict):
|
||||
# Format validation errors in a user-friendly way
|
||||
formatted_errors = []
|
||||
for field, error in validation_errors.items():
|
||||
if isinstance(error, list):
|
||||
formatted_errors.append(f"{field}: {', '.join(error)}")
|
||||
else:
|
||||
formatted_errors.append(f"{field}: {error}")
|
||||
error_message += f". Validation errors: {'; '.join(formatted_errors)}"
|
||||
else:
|
||||
error_message += f". Validation errors: {validation_errors}"
|
||||
|
||||
# Add error code if present
|
||||
if self.error_details.get("error_code"):
|
||||
error_message += f" (Error code: {self.error_details['error_code']})"
|
||||
|
||||
# Add HTTP status code information if it's not a standard success code
|
||||
if self.status_code not in (200, 202):
|
||||
error_message += f" (HTTP status: {self.status_code})"
|
||||
|
||||
return error_message
|
||||
|
||||
def log_detailed_response(self, logger_instance: logging.Logger, operation: str = "CRS operation") -> None:
|
||||
"""
|
||||
Log detailed response information for debugging purposes.
|
||||
|
||||
Args:
|
||||
logger_instance: Logger instance to use
|
||||
operation: Description of the operation being logged
|
||||
"""
|
||||
if self.success:
|
||||
logger_instance.info(f"{operation} completed successfully (HTTP {self.status_code})")
|
||||
else:
|
||||
logger_instance.error(f"{operation} failed (HTTP {self.status_code})")
|
||||
logger_instance.error(f"Response text: {self.response_text}")
|
||||
|
||||
if self.error_details:
|
||||
logger_instance.error(f"Error details: {self.error_details}")
|
||||
|
||||
# Log specific error information
|
||||
if self.error_details.get("error_message"):
|
||||
logger_instance.error(f"Error message: {self.error_details['error_message']}")
|
||||
|
||||
if self.error_details.get("error_code"):
|
||||
logger_instance.error(f"Error code: {self.error_details['error_code']}")
|
||||
|
||||
if self.error_details.get("validation_errors"):
|
||||
logger_instance.error(f"Validation errors: {self.error_details['validation_errors']}")
|
||||
|
||||
if self.error_details.get("status"):
|
||||
logger_instance.error(f"Status: {self.error_details['status']}")
|
||||
|
||||
if self.error_details.get("success_indicator") is not None:
|
||||
logger_instance.error(f"Success indicator: {self.error_details['success_indicator']}")
|
||||
|
||||
if self.error_details.get("accepted") is not None:
|
||||
logger_instance.error(f"Accepted: {self.error_details['accepted']}")
|
||||
|
||||
if self.error_details.get("valid") is not None:
|
||||
logger_instance.error(f"Valid: {self.error_details['valid']}")
|
||||
|
||||
@classmethod
|
||||
def from_response(cls, response: requests.Response) -> CRSResponse:
|
||||
"""Create a CRSResponse from a requests.Response object."""
|
||||
try:
|
||||
# Try to parse JSON response for detailed error information
|
||||
response_data = response.json()
|
||||
|
||||
# Check if the response indicates success despite HTTP status
|
||||
# Look for common error indicators in the response body
|
||||
if isinstance(response_data, dict):
|
||||
# Check for various error fields that might be present in the response
|
||||
error_message = (
|
||||
response_data.get("error", "")
|
||||
or response_data.get("message", "")
|
||||
or response_data.get("detail", "")
|
||||
or response_data.get("reason", "")
|
||||
)
|
||||
error_code = response_data.get("error_code", "") or response_data.get("code", "")
|
||||
validation_errors = (
|
||||
response_data.get("validation_errors", {})
|
||||
or response_data.get("errors", {})
|
||||
or response_data.get("field_errors", {})
|
||||
or response_data.get("constraint_violations", {})
|
||||
)
|
||||
|
||||
# Check for success/failure indicators in the response
|
||||
status = response_data.get("status", "").lower()
|
||||
success_indicator = response_data.get("success", None)
|
||||
|
||||
# Look for additional failure indicators
|
||||
has_failure_indicators = (
|
||||
error_message
|
||||
or error_code
|
||||
or validation_errors
|
||||
or status in ["error", "failed", "failure", "invalid", "rejected"]
|
||||
or success_indicator is False
|
||||
or response_data.get("accepted", True) is False
|
||||
or response_data.get("valid", True) is False
|
||||
)
|
||||
|
||||
# Determine if this is actually a success or failure
|
||||
# Even with 200/202 status, the response body might indicate failure
|
||||
is_success = response.status_code in (200, 202) and not has_failure_indicators
|
||||
|
||||
return cls(
|
||||
success=is_success,
|
||||
status_code=response.status_code,
|
||||
response_text=response.text,
|
||||
error_details={
|
||||
"error_message": error_message,
|
||||
"error_code": error_code,
|
||||
"validation_errors": validation_errors,
|
||||
"status": status,
|
||||
"success_indicator": success_indicator,
|
||||
"accepted": response_data.get("accepted"),
|
||||
"valid": response_data.get("valid"),
|
||||
"raw_response": response_data,
|
||||
},
|
||||
)
|
||||
else:
|
||||
# Non-JSON response, treat as success if status is 200/202
|
||||
return cls(
|
||||
success=response.status_code in (200, 202),
|
||||
status_code=response.status_code,
|
||||
response_text=response.text,
|
||||
)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
# Response is not valid JSON, treat as success if status is 200/202
|
||||
return cls(
|
||||
success=response.status_code in (200, 202),
|
||||
status_code=response.status_code,
|
||||
response_text=response.text,
|
||||
)
|
||||
|
||||
|
||||
class CRSClient:
|
||||
def __init__(self, crs_base_url: str, username: str | None = None, password: str | None = None) -> None:
|
||||
self.crs_base_url = crs_base_url.rstrip("/")
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def submit_task(self, task: Task) -> bool:
|
||||
def submit_task(self, task: Task) -> CRSResponse:
|
||||
"""
|
||||
Submit a task to the CRS via POST /v1/task endpoint
|
||||
|
||||
@@ -22,7 +193,7 @@ class CRSClient:
|
||||
task: Task object to submit
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
CRSResponse object with detailed status and error information
|
||||
"""
|
||||
url = f"{self.crs_base_url}/v1/task/"
|
||||
|
||||
@@ -42,18 +213,19 @@ class CRSClient:
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if response.status_code in (202, 200):
|
||||
logger.info(f"Task {task.tasks[0].task_id} submitted successfully to CRS")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to submit task to CRS. Status: {response.status_code}, Response: {response.text}")
|
||||
return False
|
||||
# Create detailed response object
|
||||
crs_response = CRSResponse.from_response(response)
|
||||
|
||||
# Log detailed response information
|
||||
crs_response.log_detailed_response(logger, f"Task submission for {task.tasks[0].task_id}")
|
||||
|
||||
return crs_response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error submitting task to CRS: {e}")
|
||||
return False
|
||||
return CRSResponse(success=False, status_code=0, response_text=str(e), error_details={"exception": str(e)})
|
||||
|
||||
def submit_sarif_broadcast(self, broadcast: SARIFBroadcast) -> bool:
|
||||
def submit_sarif_broadcast(self, broadcast: SARIFBroadcast) -> CRSResponse:
|
||||
"""
|
||||
Submit a SARIF Broadcast to the CRS via POST /v1/sarif/ endpoint
|
||||
"""
|
||||
@@ -75,18 +247,19 @@ class CRSClient:
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if response.status_code in (202, 200):
|
||||
logger.info("SARIF Broadcasts submitted successfully to CRS")
|
||||
return True
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to submit SARIF Broadcasts to CRS. Status: {response.status_code}, Response: {response.text}"
|
||||
)
|
||||
return False
|
||||
# Create detailed response object
|
||||
crs_response = CRSResponse.from_response(response)
|
||||
|
||||
# Log detailed response information
|
||||
crs_response.log_detailed_response(
|
||||
logger, f"SARIF Broadcast submission for {len(broadcast.broadcasts)} tasks"
|
||||
)
|
||||
|
||||
return crs_response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error submitting SARIF Broadcasts to CRS: {e}")
|
||||
return False
|
||||
return CRSResponse(success=False, status_code=0, response_text=str(e), error_details={"exception": str(e)})
|
||||
|
||||
def ping(self) -> bool:
|
||||
"""
|
||||
|
||||
@@ -43,6 +43,12 @@ class Task(Base):
|
||||
fuzz_tooling_ref: Mapped[str] = mapped_column(String)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
||||
|
||||
# Error tracking fields
|
||||
crs_submission_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
crs_error_details: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON string for detailed error info
|
||||
crs_submission_status: Mapped[str | None] = mapped_column(String, nullable=True) # 'pending', 'success', 'failed'
|
||||
crs_submission_timestamp: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
povs: Mapped[list["POV"]] = relationship("POV", back_populates="task", cascade="all, delete-orphan")
|
||||
patches: Mapped[list["Patch"]] = relationship("Patch", back_populates="task", cascade="all, delete-orphan")
|
||||
@@ -133,6 +139,35 @@ class DatabaseManager:
|
||||
yield task
|
||||
session.commit()
|
||||
|
||||
def get_tasks_by_crs_status(self, crs_status: str) -> list[Task]:
|
||||
"""Get all tasks with a specific CRS submission status."""
|
||||
with self.get_session() as session:
|
||||
tasks = session.query(Task).filter(Task.crs_submission_status == crs_status).all()
|
||||
session.commit()
|
||||
return tasks
|
||||
|
||||
def get_tasks_by_status(self, status: str) -> list[Task]:
|
||||
"""Get all tasks with a specific overall status (active, expired, failed)."""
|
||||
with self.get_session() as session:
|
||||
if status == "failed":
|
||||
# For failed status, check CRS submission status
|
||||
tasks = session.query(Task).filter(Task.crs_submission_status == "failed").all()
|
||||
else:
|
||||
# For other statuses, we need to calculate based on deadline
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
if status == "active":
|
||||
tasks = session.query(Task).filter(Task.deadline > now).all()
|
||||
elif status == "expired":
|
||||
tasks = session.query(Task).filter(Task.deadline <= now).all()
|
||||
else:
|
||||
tasks = []
|
||||
|
||||
session.commit()
|
||||
return tasks
|
||||
|
||||
@contextmanager
|
||||
def get_povs_for_task(self, task_id: str) -> Iterator[list[POV]]:
|
||||
"""Context manager: yields all POVs for a task with session open."""
|
||||
@@ -250,6 +285,32 @@ class DatabaseManager:
|
||||
logger.info(f"Created task: {task.task_id}")
|
||||
return task
|
||||
|
||||
def update_task_crs_status(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
crs_submission_status: str,
|
||||
crs_submission_error: str | None = None,
|
||||
crs_error_details: dict | None = None,
|
||||
) -> None:
|
||||
"""Update the CRS submission status and error information for a task."""
|
||||
import json
|
||||
|
||||
with self.get_session() as session:
|
||||
task = session.query(Task).filter(Task.task_id == task_id).first()
|
||||
if task:
|
||||
task.crs_submission_status = crs_submission_status
|
||||
task.crs_submission_error = crs_submission_error
|
||||
task.crs_submission_timestamp = datetime.now()
|
||||
|
||||
if crs_error_details:
|
||||
task.crs_error_details = json.dumps(crs_error_details, indent=2)
|
||||
|
||||
session.commit()
|
||||
logger.info(f"Updated CRS status for task {task_id}: {crs_submission_status}")
|
||||
else:
|
||||
logger.warning(f"Task {task_id} not found for CRS status update")
|
||||
|
||||
# POV operations
|
||||
def create_pov(
|
||||
self, *, task_id: str, architecture: str, engine: str, fuzzer_name: str, sanitizer: str, testcase: bytes
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
<div class="stat-number" id="active-tasks">-</div>
|
||||
<div class="stat-label">Currently running</div>
|
||||
</div>
|
||||
<div class="card failed-tasks" disabled>
|
||||
<h3>Failed Tasks</h3>
|
||||
<div class="stat-number" id="failed-tasks">-</div>
|
||||
<div class="stat-label">CRS submissions failed</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Total PoVs</h3>
|
||||
<div class="stat-number" id="total-povs">-</div>
|
||||
@@ -61,6 +66,7 @@
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="expired">Expired</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,13 +32,15 @@ const elements = {
|
||||
patchesContainer: document.getElementById('patches-container'),
|
||||
statusFilter: document.getElementById('status-filter'),
|
||||
activeTasks: document.getElementById('active-tasks'),
|
||||
failedTasks: document.getElementById('failed-tasks'),
|
||||
totalPovs: document.getElementById('total-povs'),
|
||||
totalPatches: document.getElementById('total-patches'),
|
||||
totalBundles: document.getElementById('total-bundles'),
|
||||
detailTitle: document.getElementById('detail-title'),
|
||||
detailContent: document.getElementById('detail-content'),
|
||||
tabButtons: document.querySelectorAll('.tab-button'),
|
||||
tabPanes: document.querySelectorAll('.tab-pane')
|
||||
tabPanes: document.querySelectorAll('.tab-pane'),
|
||||
notifications: document.getElementById('notifications')
|
||||
};
|
||||
|
||||
// Initialize the dashboard
|
||||
@@ -205,7 +207,8 @@ async function loadDashboard() {
|
||||
// Load tasks from API
|
||||
async function loadTasks() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/v1/dashboard/tasks`);
|
||||
// Add timestamp to prevent caching
|
||||
const response = await fetch(`${API_BASE}/v1/dashboard/tasks?t=${Date.now()}`);
|
||||
if (response.ok) {
|
||||
tasks = await response.json();
|
||||
} else {
|
||||
@@ -308,6 +311,7 @@ function calculateStatsFromTasks() {
|
||||
function updateDashboard() {
|
||||
// Update stats
|
||||
elements.activeTasks.textContent = dashboardStats.activeTasks;
|
||||
elements.failedTasks.textContent = dashboardStats.failedTasks || 0;
|
||||
elements.totalPovs.textContent = dashboardStats.totalPovs;
|
||||
elements.totalPatches.textContent = dashboardStats.totalPatches;
|
||||
elements.totalBundles.textContent = dashboardStats.totalBundles;
|
||||
@@ -336,6 +340,11 @@ async function loadAndRenderPatches() {
|
||||
|
||||
// Render PoVs list
|
||||
function renderPovs() {
|
||||
if (!elements.povsContainer) {
|
||||
console.error('PoVs container not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (allPovs.length === 0) {
|
||||
elements.povsContainer.innerHTML = `
|
||||
<div class="no-data">
|
||||
@@ -395,20 +404,26 @@ function renderPatches() {
|
||||
|
||||
// Render tasks list
|
||||
function renderTasks() {
|
||||
const filteredTasks = filterTasksByStatus();
|
||||
if (!elements.tasksContainer) {
|
||||
console.error('Tasks container not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredTasks = filterTasksByStatus(tasks);
|
||||
|
||||
if (filteredTasks.length === 0) {
|
||||
elements.tasksContainer.innerHTML = `
|
||||
<div class="no-data">
|
||||
<div class="no-data-icon">📋</div>
|
||||
<div class="no-data-icon">📝</div>
|
||||
<p>No tasks found</p>
|
||||
<p class="no-data-subtitle">Create a new task to get started</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
elements.tasksContainer.innerHTML = filteredTasks.map(task => `
|
||||
<div class="task-item" onclick="showTaskDetail('${task.task_id}')">
|
||||
<div class="task-item ${task.crs_submission_status === 'failed' ? 'task-failed' : ''}" onclick="showTaskDetail('${task.task_id}')">
|
||||
<div class="task-info">
|
||||
<div class="task-name">${task.name || task.project_name}</div>
|
||||
<div class="task-id">ID: ${task.task_id}</div>
|
||||
@@ -418,6 +433,11 @@ function renderTasks() {
|
||||
<span>Created: ${formatTimestamp(task.created_at)}</span>
|
||||
<span>Deadline: ${formatTimestamp(task.deadline)}</span>
|
||||
</div>
|
||||
${task.crs_submission_error ? `
|
||||
<div class="crs-error-details">
|
||||
<strong>Error:</strong> ${task.crs_submission_error}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="task-status">
|
||||
<span class="status-badge status-${task.status}">${task.status}</span>
|
||||
@@ -482,12 +502,29 @@ async function handleExampleTaskSubmission() {
|
||||
body: JSON.stringify(exampleTaskData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Example task submission response:', result);
|
||||
console.log('Response message:', result.message);
|
||||
console.log('Response color:', result.color);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
showNotification('Example libpng task submitted successfully!', 'success');
|
||||
|
||||
// Refresh dashboard after a short delay
|
||||
setTimeout(loadDashboard, 1000);
|
||||
// Check if the response indicates a CRS submission failure or setup failure
|
||||
if (result.message && (result.message.includes('failed to submit to CRS') || result.message.includes('failed during setup'))) {
|
||||
console.log('Example task creation/submission failed, showing error notification');
|
||||
// Task was created but failed - show notification with proper color
|
||||
showNotification(result.message, null, result.color);
|
||||
|
||||
// Force refresh failed tasks and main tasks list
|
||||
setTimeout(async () => {
|
||||
await forceRefreshFailedTasks();
|
||||
}, 1000);
|
||||
} else {
|
||||
// Complete success
|
||||
showNotification(result.message || 'Example libpng task submitted successfully!', 'success');
|
||||
|
||||
// Refresh dashboard after a short delay
|
||||
setTimeout(loadDashboard, 1000);
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showNotification(`Error: ${error.message || 'Failed to submit example task'}`, 'error');
|
||||
@@ -527,17 +564,37 @@ async function handleTaskSubmission(event) {
|
||||
body: JSON.stringify(taskData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Task submission response:', result);
|
||||
console.log('Response message:', result.message);
|
||||
console.log('Response color:', result.color);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
showNotification('Task submitted successfully!', 'success');
|
||||
elements.taskModal.style.display = 'none';
|
||||
elements.taskForm.reset();
|
||||
|
||||
// Refresh dashboard after a short delay
|
||||
setTimeout(loadDashboard, 1000);
|
||||
// Check if the response indicates a CRS submission failure or setup failure
|
||||
if (result.message && (result.message.includes('failed to submit to CRS') || result.message.includes('failed during setup'))) {
|
||||
console.log('Task creation/submission failed, showing error notification');
|
||||
// Task was created but failed - show notification with proper color
|
||||
showNotification(result.message, null, result.color);
|
||||
elements.taskModal.style.display = 'none';
|
||||
elements.taskForm.reset();
|
||||
|
||||
// Force refresh failed tasks and main tasks list
|
||||
setTimeout(async () => {
|
||||
await forceRefreshFailedTasks();
|
||||
}, 1000);
|
||||
} else {
|
||||
// Complete success
|
||||
showNotification(result.message || 'Task submitted successfully!', 'success');
|
||||
elements.taskModal.style.display = 'none';
|
||||
elements.taskForm.reset();
|
||||
|
||||
// Refresh dashboard after a short delay
|
||||
setTimeout(loadDashboard, 1000);
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showNotification(`Error: ${error.message || 'Failed to submit task'}`, 'error');
|
||||
// HTTP error - show error message
|
||||
const errorMessage = result.message || result.detail || 'Failed to submit task';
|
||||
showNotification(`Error: ${errorMessage}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting task:', error);
|
||||
@@ -578,28 +635,20 @@ function renderTaskDetail(task) {
|
||||
<div class="detail-section">
|
||||
<h3>Task Information</h3>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-label">Task ID:</div>
|
||||
<div class="detail-value">${task.task_id}</div>
|
||||
<div class="detail-label">Name:</div>
|
||||
<div class="detail-value">${task.name || 'N/A'}</div>
|
||||
<div class="detail-value">${task.name || task.project_name}</div>
|
||||
<div class="detail-label">ID:</div>
|
||||
<div class="detail-value">${task.task_id}</div>
|
||||
<div class="detail-label">Project:</div>
|
||||
<div class="detail-value">${task.project_name}</div>
|
||||
<div class="detail-label">Status:</div>
|
||||
<div class="detail-value">
|
||||
<span class="status-badge status-${task.status}">${task.status}</span>
|
||||
</div>
|
||||
<div class="detail-label">Duration:</div>
|
||||
<div class="detail-value">${formatDuration(task.duration)}</div>
|
||||
<div class="detail-label">Created:</div>
|
||||
<div class="detail-value">${formatTimestamp(task.created_at)}</div>
|
||||
<div class="detail-label">Deadline:</div>
|
||||
<div class="detail-value">${formatTimestamp(task.deadline)}</div>
|
||||
<div class="detail-label">Repository:</div>
|
||||
<div class="detail-value">${task.challenge_repo_url || 'N/A'}</div>
|
||||
<div class="detail-label">Head Ref:</div>
|
||||
<div class="detail-value">${task.challenge_repo_head_ref || 'N/A'}</div>
|
||||
<div class="detail-label">Base Ref:</div>
|
||||
<div class="detail-value">${task.challenge_repo_base_ref || 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -825,6 +874,9 @@ function renderArtifactDetail(detailData, type) {
|
||||
// Not base64, use as is
|
||||
}
|
||||
}
|
||||
const patchPreview = patchContent.length > 300
|
||||
? patchContent.substring(0, 300) + '...'
|
||||
: patchContent;
|
||||
specificContent = `
|
||||
<div class="detail-label">Patch Size:</div>
|
||||
<div class="detail-value">${originalSize} characters (${patchContent.length} decoded)</div>
|
||||
@@ -879,44 +931,29 @@ function formatTimestamp(timestamp) {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
// Create a simple notification system
|
||||
function showNotification(message, type = 'info', color = null) {
|
||||
// If color is provided from backend Message, use it to override type
|
||||
if (color === 'error') {
|
||||
type = 'error';
|
||||
} else if (color === 'warning') {
|
||||
type = 'warning';
|
||||
} else if (color === 'success') {
|
||||
type = 'success';
|
||||
}
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
// Add notification styles if not already added
|
||||
if (!document.querySelector('style[data-notifications]')) {
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('data-notifications', 'true');
|
||||
style.textContent = `
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 2000;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
.notification-success { background-color: #4caf50; }
|
||||
.notification-error { background-color: #f44336; }
|
||||
.notification-info { background-color: #2196f3; }
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
// Add to notifications container
|
||||
const container = document.getElementById('notifications') || document.body;
|
||||
container.appendChild(notification);
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Remove notification after 5 seconds
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
@@ -1019,4 +1056,4 @@ function getMockTasks() {
|
||||
bundles: []
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -55,15 +55,26 @@ body {
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card.failed-tasks {
|
||||
border-left: 4px solid #f44336;
|
||||
background: linear-gradient(135deg, #fff 0%, #ffebee 100%);
|
||||
}
|
||||
|
||||
.card.failed-tasks:hover {
|
||||
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
@@ -148,13 +159,22 @@ body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.filter-controls select {
|
||||
@@ -215,29 +235,160 @@ body {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Task status badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-expired {
|
||||
background-color: #fce4ec;
|
||||
color: #c2185b;
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* CRS status indicators */
|
||||
.crs-error-info,
|
||||
.crs-success-info,
|
||||
.crs-pending-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.crs-error-info {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
border: 1px solid #ffcdd2;
|
||||
}
|
||||
|
||||
.crs-success-info {
|
||||
background-color: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
border: 1px solid #c8e6c9;
|
||||
}
|
||||
|
||||
.crs-pending-info {
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
border: 1px solid #ffcc02;
|
||||
}
|
||||
|
||||
.crs-error-icon,
|
||||
.crs-success-icon,
|
||||
.crs-pending-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Failed task styling */
|
||||
.task-failed {
|
||||
border-left: 4px solid #f44336;
|
||||
background-color: #ffebee;
|
||||
}
|
||||
|
||||
.task-failed:hover {
|
||||
background-color: #ffcdd2;
|
||||
}
|
||||
|
||||
/* CRS error details */
|
||||
.crs-error-details {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: #fff3e0;
|
||||
border: 1px solid #ffcc02;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.crs-error-details strong {
|
||||
color: #bf360c;
|
||||
}
|
||||
|
||||
/* Notification system */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 2000;
|
||||
animation: slideIn 0.3s ease;
|
||||
max-width: 400px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Error message styling for backend errors */
|
||||
.error-message {
|
||||
color: #D8000C !important;
|
||||
background-color: #FFBABA !important;
|
||||
border: 1px solid #D8000C !important;
|
||||
padding: 10px !important;
|
||||
margin: 10px 0 !important;
|
||||
border-radius: 5px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.4 !important;
|
||||
box-shadow: 0 2px 8px rgba(216, 0, 12, 0.15) !important;
|
||||
}
|
||||
|
||||
.error-message::before {
|
||||
content: '⚠' !important;
|
||||
font-size: 20px !important;
|
||||
margin-right: 10px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
/* Make dashboard stats clickable */
|
||||
.task-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -483,14 +634,20 @@ form {
|
||||
/* No data states */
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
padding: 3rem 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-data-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.no-data-subtitle {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Artifacts container */
|
||||
@@ -609,15 +766,4 @@ form {
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Make dashboard stats clickable */
|
||||
.status-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.status-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class TestCRSClient:
|
||||
|
||||
result = crs_client.submit_task(sample_task)
|
||||
|
||||
assert result is True
|
||||
assert result.success is True
|
||||
|
||||
# Verify request was made correctly
|
||||
mock_post.assert_called_once_with(
|
||||
@@ -76,7 +76,7 @@ class TestCRSClient:
|
||||
|
||||
result = crs_client_no_auth.submit_task(sample_task)
|
||||
|
||||
assert result is True
|
||||
assert result.success is True
|
||||
|
||||
# Verify request was made without auth
|
||||
mock_post.assert_called_once_with(
|
||||
@@ -98,7 +98,7 @@ class TestCRSClient:
|
||||
|
||||
result = crs_client.submit_task(sample_task)
|
||||
|
||||
assert result is False
|
||||
assert result.success is False
|
||||
|
||||
@patch("requests.post")
|
||||
def test_submit_task_exception(self, mock_post, crs_client, sample_task):
|
||||
@@ -108,7 +108,7 @@ class TestCRSClient:
|
||||
|
||||
result = crs_client.submit_task(sample_task)
|
||||
|
||||
assert result is False
|
||||
assert result.success is False
|
||||
|
||||
@patch("requests.get")
|
||||
def test_ping_success_ready(self, mock_get, crs_client):
|
||||
|
||||
Reference in New Issue
Block a user