From 9405bd842e7556b08af8ac4f786d546a0bee0134 Mon Sep 17 00:00:00 2001 From: Hassan Pasha Date: Fri, 3 Apr 2026 13:30:43 -0500 Subject: [PATCH 1/7] fix: prevent simulation destruction on page refresh + add dashboard view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browser refresh was killing running simulations due to two compounding bugs: the frontend unconditionally called /start on mount with force=true hardcoded, nuking the running process and deleting all data files every time. Bug fixes: - Frontend: check run-status before starting; only start if not already running - Frontend: change force flag from true to false in default start path - Backend: pass GraphStorage to SimulationRunner.start_simulation() so graph memory updates actually work (was failing silently with "Must provide storage") - Backend: preserve state.json as "running" when /start returns 400 for already-running sim (prevents frontend retry loop from state desync) - Backend: clear Neo4j graph data during force-restart cleanup (was leaving stale nodes/edges from previous runs) Additional fixes applied during debugging: - Bump default LLM context window from 8192 to 32768 tokens - Add traceback logging for ontology generation failures - Filter malformed entity/edge types missing 'name' key New feature — simulation dashboard (/dashboard): - "Active Now" section with live-updating cards for running simulations (progress bars, round counts, stop/view actions, 3s polling) - "All Simulations" history table with search and status filter tabs - Added to help verify the bug fixes were working correctly Made-with: Cursor --- backend/app/api/graph.py | 2 + backend/app/api/simulation.py | 33 +- backend/app/services/ontology_generator.py | 6 + backend/app/services/simulation_runner.py | 16 +- backend/app/utils/llm_client.py | 2 +- frontend/src/components/Step3Simulation.vue | 17 +- frontend/src/router/index.js | 6 + frontend/src/views/DashboardView.vue | 712 ++++++++++++++++++++ frontend/src/views/Home.vue | 4 +- 9 files changed, 783 insertions(+), 15 deletions(-) create mode 100644 frontend/src/views/DashboardView.vue diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index 59f915e..59ae0df 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -255,6 +255,8 @@ def generate_ontology(): }) except Exception as e: + logger.error(f"Ontology generation failed: {e}") + logger.error(traceback.format_exc()) return jsonify({ "success": False, "error": str(e), diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 274ee79..a2fadf6 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -1532,40 +1532,44 @@ def start_simulation(): force_restarted = False - # Intelligently handle status: if preparation work is complete, reset status to ready if state.status != SimulationStatus.READY: # Check if preparation is complete is_prepared, prepare_info = _check_simulation_prepared(simulation_id) if is_prepared: - # Preparation work complete, verify if simulation is not already running + # Check if simulation process is really running if state.status == SimulationStatus.RUNNING: - # Check if simulation process is really running run_state = SimulationRunner.get_run_state(simulation_id) if run_state and run_state.runner_status.value == "running": - # Process is indeed running if force: - # Force mode:Stop runningSimulation - logger.info(f"Force mode:Stop runningSimulation {simulation_id}") + logger.info(f"Force mode: stopping running simulation {simulation_id}") try: SimulationRunner.stop_simulation(simulation_id) except Exception as e: logger.warning(f"Warning when stopping simulation: {str(e)}") else: + # Ensure state.json reflects running status + manager._save_simulation_state(state) return jsonify({ "success": False, "error": f"Simulation is running. Please call /stop first or use force=true to force restart." }), 400 - # If force mode,Clean runtime logs + # If force mode, clean runtime logs and Neo4j graph data if force: logger.info(f"Force mode: cleaning simulation runtime files for {simulation_id}") - cleanup_result = SimulationRunner.cleanup_simulation_logs(simulation_id) + cleanup_storage = current_app.extensions.get('neo4j_storage') + cleanup_graph_id = state.graph_id + cleanup_result = SimulationRunner.cleanup_simulation_logs( + simulation_id, + storage=cleanup_storage, + graph_id=cleanup_graph_id + ) if not cleanup_result.get("success"): logger.warning(f"Warning when cleaning logs: {cleanup_result.get('errors')}") force_restarted = True - # Process does not exist or has ended,Reset status to ready + # Process does not exist or has ended, reset status to ready logger.info(f"Simulation {simulation_id} preparation complete, resetting status to ready (previous status: {state.status.value})") state.status = SimulationStatus.READY manager._save_simulation_state(state) @@ -1595,13 +1599,22 @@ def start_simulation(): logger.info(f"Enable knowledge graph memory update: simulation_id={simulation_id}, graph_id={graph_id}") + # Get GraphStorage for graph memory update + storage = None + if enable_graph_memory_update: + storage = current_app.extensions.get('neo4j_storage') + if not storage: + logger.warning("GraphStorage not available, graph memory update will be disabled") + enable_graph_memory_update = False + # Start simulation run_state = SimulationRunner.start_simulation( simulation_id=simulation_id, platform=platform, max_rounds=max_rounds, enable_graph_memory_update=enable_graph_memory_update, - graph_id=graph_id + graph_id=graph_id, + storage=storage ) # Update simulation status diff --git a/backend/app/services/ontology_generator.py b/backend/app/services/ontology_generator.py index 25a5aba..95b430c 100644 --- a/backend/app/services/ontology_generator.py +++ b/backend/app/services/ontology_generator.py @@ -265,6 +265,9 @@ def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: if "analysis_summary" not in result: result["analysis_summary"] = "" + # Filter out malformed entity types (missing 'name' key) + result["entity_types"] = [e for e in result["entity_types"] if "name" in e] + # Validate entity types for entity in result["entity_types"]: if "attributes" not in entity: @@ -275,6 +278,9 @@ def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: if len(entity.get("description", "")) > 100: entity["description"] = entity["description"][:97] + "..." + # Filter out malformed edge types (missing 'name' key) + result["edge_types"] = [e for e in result["edge_types"] if "name" in e] + # Validate relationship types for edge in result["edge_types"]: if "source_targets" not in edge: diff --git a/backend/app/services/simulation_runner.py b/backend/app/services/simulation_runner.py index 54215b5..5547683 100644 --- a/backend/app/services/simulation_runner.py +++ b/backend/app/services/simulation_runner.py @@ -1098,7 +1098,7 @@ def get_agent_stats(cls, simulation_id: str) -> List[Dict[str, Any]]: return result @classmethod - def cleanup_simulation_logs(cls, simulation_id: str) -> Dict[str, Any]: + def cleanup_simulation_logs(cls, simulation_id: str, storage=None, graph_id: str = None) -> Dict[str, Any]: """ Clean up simulation run logs (for force restart) @@ -1112,10 +1112,14 @@ def cleanup_simulation_logs(cls, simulation_id: str) -> Dict[str, Any]: - reddit_simulation.db (simulation database) - env_status.json (environment status) + If storage and graph_id are provided, also clears Neo4j graph data. + Note: Does not delete config files (simulation_config.json) and profile files Args: simulation_id: Simulation ID + storage: Optional GraphStorage instance for Neo4j cleanup + graph_id: Optional graph ID to clear in Neo4j Returns: Cleanup result information @@ -1170,6 +1174,16 @@ def cleanup_simulation_logs(cls, simulation_id: str) -> Dict[str, Any]: if simulation_id in cls._run_states: del cls._run_states[simulation_id] + # Clean up Neo4j graph data if storage provided + if storage and graph_id: + try: + storage.delete_graph(graph_id) + cleaned_files.append(f"neo4j:graph:{graph_id}") + logger.info(f"Cleared Neo4j graph data: graph_id={graph_id}") + except Exception as e: + errors.append(f"Failed to clear Neo4j graph: {str(e)}") + logger.error(f"Failed to clear Neo4j graph data: {e}") + logger.info(f"Cleanup simulation logs completed: {simulation_id}, deleted files: {cleaned_files}") return { diff --git a/backend/app/utils/llm_client.py b/backend/app/utils/llm_client.py index 9a2d926..512d214 100644 --- a/backend/app/utils/llm_client.py +++ b/backend/app/utils/llm_client.py @@ -38,7 +38,7 @@ def __init__( # Ollama context window size — prevents prompt truncation. # Read from env OLLAMA_NUM_CTX, default 8192 (Ollama default is only 2048). - self._num_ctx = int(os.environ.get('OLLAMA_NUM_CTX', '8192')) + self._num_ctx = int(os.environ.get('OLLAMA_NUM_CTX', '32768')) def _is_ollama(self) -> bool: """Check if we're talking to an Ollama server.""" diff --git a/frontend/src/components/Step3Simulation.vue b/frontend/src/components/Step3Simulation.vue index dcd9bd8..0e4ba5d 100644 --- a/frontend/src/components/Step3Simulation.vue +++ b/frontend/src/components/Step3Simulation.vue @@ -395,7 +395,7 @@ const doStartSimulation = async () => { const params = { simulation_id: props.simulationId, platform: 'parallel', - force: true, // Force restart + force: false, enable_graph_memory_update: true // Enable dynamic graph update } @@ -684,9 +684,22 @@ watch(() => props.systemLogs?.length, () => { }) }) -onMounted(() => { +onMounted(async () => { addLog('Step3 Simulation initialization') if (props.simulationId) { + try { + const res = await getRunStatus(props.simulationId) + if (res.success && res.data && res.data.runner_status === 'running') { + addLog('Simulation already running — resuming status polling') + runStatus.value = res.data + phase.value = 1 + startStatusPolling() + startDetailPolling() + return + } + } catch (e) { + // No running simulation found, safe to start + } doStartSimulation() } }) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 62d2320..e205deb 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -5,6 +5,7 @@ import SimulationView from '../views/SimulationView.vue' import SimulationRunView from '../views/SimulationRunView.vue' import ReportView from '../views/ReportView.vue' import InteractionView from '../views/InteractionView.vue' +import DashboardView from '../views/DashboardView.vue' const routes = [ { @@ -12,6 +13,11 @@ const routes = [ name: 'Home', component: Home }, + { + path: '/dashboard', + name: 'Dashboard', + component: DashboardView + }, { path: '/process/:projectId', name: 'Process', diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue new file mode 100644 index 0000000..cbbf9df --- /dev/null +++ b/frontend/src/views/DashboardView.vue @@ -0,0 +1,712 @@ + + + + + diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index 36bb714..22c9758 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -4,6 +4,7 @@