diff --git a/Dockerfile b/Dockerfile index cfd2734..9ccde18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ COPY backend/requirements.txt /app/backend/requirements.txt RUN pip install --no-cache-dir -r /app/backend/requirements.txt +RUN python -m playwright install --with-deps chromium COPY backend/ /app/backend/ COPY --from=frontend-build /app/frontend/build /app/frontend/build diff --git a/backend/requirements.txt b/backend/requirements.txt index 88d7956..ed3ba6b 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -53,4 +53,5 @@ itsdangerous email-validator openai playwright -tenacity \ No newline at end of file +tenacity +Pillow diff --git a/backend/src/core/task_manager/manager.py b/backend/src/core/task_manager/manager.py index 9c52092..759c996 100644 --- a/backend/src/core/task_manager/manager.py +++ b/backend/src/core/task_manager/manager.py @@ -11,7 +11,7 @@ import asyncio import threading import inspect from concurrent.futures import ThreadPoolExecutor -from datetime import datetime +from datetime import datetime, timezone from typing import Dict, Any, List, Optional from .models import Task, TaskStatus, LogEntry, LogFilter, LogStats @@ -329,8 +329,18 @@ class TaskManager: tasks = [t for t in tasks if t.plugin_id in plugin_id_set] if completed_only: tasks = [t for t in tasks if t.status in [TaskStatus.SUCCESS, TaskStatus.FAILED]] - # Sort by start_time descending (most recent first) - tasks.sort(key=lambda t: t.started_at or datetime.min, reverse=True) + # Sort by started_at descending with tolerant handling of mixed tz-aware/naive values. + def sort_key(task: Task) -> float: + started_at = task.started_at + if started_at is None: + return float("-inf") + if not isinstance(started_at, datetime): + return float("-inf") + if started_at.tzinfo is None: + return started_at.replace(tzinfo=timezone.utc).timestamp() + return started_at.timestamp() + + tasks.sort(key=sort_key, reverse=True) return tasks[offset:offset + limit] # [/DEF:get_tasks:Function] diff --git a/backend/src/core/task_manager/models.py b/backend/src/core/task_manager/models.py index 56f6cc2..c49b094 100644 --- a/backend/src/core/task_manager/models.py +++ b/backend/src/core/task_manager/models.py @@ -109,7 +109,8 @@ class Task(BaseModel): params: Dict[str, Any] = Field(default_factory=dict) input_required: bool = False input_request: Optional[Dict[str, Any]] = None - result: Optional[Dict[str, Any]] = None + # Result payload can be dict/list/scalar depending on plugin and legacy records. + result: Optional[Any] = None # [DEF:__init__:Function] # @PURPOSE: Initializes the Task model and validates input_request for AWAITING_INPUT status. @@ -123,4 +124,4 @@ class Task(BaseModel): # [/DEF:__init__:Function] # [/DEF:Task:Class] -# [/DEF:TaskManagerModels:Module] \ No newline at end of file +# [/DEF:TaskManagerModels:Module] diff --git a/backend/src/core/task_manager/persistence.py b/backend/src/core/task_manager/persistence.py index c6b1951..743eee0 100644 --- a/backend/src/core/task_manager/persistence.py +++ b/backend/src/core/task_manager/persistence.py @@ -12,6 +12,7 @@ import json from sqlalchemy.orm import Session from ...models.task import TaskRecord, TaskLogRecord +from ...models.mapping import Environment from ..database import TasksSessionLocal from .models import Task, TaskStatus, LogEntry, TaskLog, LogFilter, LogStats from ..logger import logger, belief_scope @@ -21,6 +22,40 @@ from ..logger import logger, belief_scope # @SEMANTICS: persistence, service, database, sqlalchemy # @PURPOSE: Provides methods to save and load tasks from the tasks.db database using SQLAlchemy. class TaskPersistenceService: + @staticmethod + def _json_load_if_needed(value): + if value is None: + return None + if isinstance(value, (dict, list)): + return value + if isinstance(value, str): + stripped = value.strip() + if stripped == "" or stripped.lower() == "null": + return None + try: + return json.loads(stripped) + except json.JSONDecodeError: + return value + return value + + @staticmethod + def _parse_datetime(value): + if value is None or isinstance(value, datetime): + return value + if isinstance(value, str): + try: + return datetime.fromisoformat(value) + except ValueError: + return None + return None + + @staticmethod + def _resolve_environment_id(session: Session, env_id: Optional[str]) -> Optional[str]: + if not env_id: + return None + exists = session.query(Environment.id).filter(Environment.id == env_id).first() + return env_id if exists else None + # [DEF:__init__:Function] # @PURPOSE: Initializes the persistence service. # @PRE: None. @@ -48,7 +83,8 @@ class TaskPersistenceService: record.type = task.plugin_id record.status = task.status.value - record.environment_id = task.params.get("environment_id") or task.params.get("source_env_id") + raw_env_id = task.params.get("environment_id") or task.params.get("source_env_id") + record.environment_id = self._resolve_environment_id(session, raw_env_id) record.started_at = task.started_at record.finished_at = task.finished_at @@ -123,21 +159,28 @@ class TaskPersistenceService: for record in records: try: logs = [] - if record.logs: - for log_data in record.logs: - # Handle timestamp conversion if it's a string - if isinstance(log_data.get('timestamp'), str): - log_data['timestamp'] = datetime.fromisoformat(log_data['timestamp']) + logs_payload = self._json_load_if_needed(record.logs) + if isinstance(logs_payload, list): + for log_data in logs_payload: + if not isinstance(log_data, dict): + continue + log_data = dict(log_data) + log_data['timestamp'] = self._parse_datetime(log_data.get('timestamp')) or datetime.utcnow() logs.append(LogEntry(**log_data)) + started_at = self._parse_datetime(record.started_at) + finished_at = self._parse_datetime(record.finished_at) + params = self._json_load_if_needed(record.params) + result = self._json_load_if_needed(record.result) + task = Task( id=record.id, plugin_id=record.type, status=TaskStatus(record.status), - started_at=record.started_at, - finished_at=record.finished_at, - params=record.params or {}, - result=record.result, + started_at=started_at, + finished_at=finished_at, + params=params if isinstance(params, dict) else {}, + result=result, logs=logs ) loaded_tasks.append(task) @@ -381,4 +424,4 @@ class TaskLogPersistenceService: # [/DEF:delete_logs_for_tasks:Function] # [/DEF:TaskLogPersistenceService:Class] -# [/DEF:TaskPersistenceModule:Module] \ No newline at end of file +# [/DEF:TaskPersistenceModule:Module] diff --git a/backend/src/plugins/llm_analysis/plugin.py b/backend/src/plugins/llm_analysis/plugin.py index dcc473f..d3583a5 100644 --- a/backend/src/plugins/llm_analysis/plugin.py +++ b/backend/src/plugins/llm_analysis/plugin.py @@ -74,7 +74,8 @@ class DashboardValidationPlugin(PluginBase): log.info(f"Executing {self.name} with params: {params}") - dashboard_id = params.get("dashboard_id") + dashboard_id_raw = params.get("dashboard_id") + dashboard_id = str(dashboard_id_raw) if dashboard_id_raw is not None else None env_id = params.get("environment_id") provider_id = params.get("provider_id")