diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index cc0a9053..aff1e5ac 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -489,6 +489,7 @@ def build_graph(): # Update project status project.status = ProjectStatus.GRAPH_BUILDING project.graph_build_task_id = task_id + project.active_task_id = task_id ProjectManager.save_project(project) # Capture locale before spawning background thread @@ -592,6 +593,7 @@ def build_graph(): # Update project status project.status = ProjectStatus.GRAPH_COMPLETED + project.active_task_id = None ProjectManager.save_project(project) node_count = graph_data.get("node_count", 0) @@ -620,6 +622,7 @@ def build_graph(): project.status = ProjectStatus.FAILED project.error = str(e) + project.active_task_id = None ProjectManager.save_project(project) task_manager.update_task( diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 697d2856..e1210da6 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -52,6 +52,9 @@ class Project: # Error info error: Optional[str] = None + # Persisted so the frontend can reconnect after a page refresh + active_task_id: Optional[str] = None + def to_dict(self) -> Dict[str, Any]: """Convert to dictionary""" return { @@ -69,7 +72,8 @@ class Project: "simulation_requirement": self.simulation_requirement, "chunk_size": self.chunk_size, "chunk_overlap": self.chunk_overlap, - "error": self.error + "error": self.error, + "active_task_id": self.active_task_id, } @classmethod @@ -94,7 +98,8 @@ class Project: simulation_requirement=data.get('simulation_requirement'), chunk_size=data.get('chunk_size', 500), chunk_overlap=data.get('chunk_overlap', 50), - error=data.get('error') + error=data.get('error'), + active_task_id=data.get('active_task_id'), ) diff --git a/backend/tests/test_project_task_recovery.py b/backend/tests/test_project_task_recovery.py new file mode 100644 index 00000000..19637750 --- /dev/null +++ b/backend/tests/test_project_task_recovery.py @@ -0,0 +1,31 @@ +def test_project_serializes_active_task_id(): + """active_task_id is included in Project.to_dict().""" + from app.models.project import Project, ProjectStatus + p = Project( + project_id="proj-1", name="Test", + status=ProjectStatus.GRAPH_BUILDING, + created_at="2026-01-01", updated_at="2026-01-01", + active_task_id="task-abc-123", + ) + assert p.to_dict()["active_task_id"] == "task-abc-123" + + +def test_project_deserializes_active_task_id(): + """Project.from_dict() restores active_task_id from JSON.""" + from app.models.project import Project + data = { + "project_id": "proj-1", "name": "Test", "status": "graph_building", + "created_at": "2026-01-01", "updated_at": "2026-01-01", + "active_task_id": "task-abc-123", + } + assert Project.from_dict(data).active_task_id == "task-abc-123" + + +def test_project_active_task_id_defaults_none(): + """active_task_id defaults to None for projects without it (backward compat).""" + from app.models.project import Project + data = { + "project_id": "proj-1", "name": "Test", "status": "created", + "created_at": "2026-01-01", "updated_at": "2026-01-01", + } + assert Project.from_dict(data).active_task_id is None diff --git a/frontend/src/views/MainView.vue b/frontend/src/views/MainView.vue index 7cfd05e5..8681f899 100644 --- a/frontend/src/views/MainView.vue +++ b/frontend/src/views/MainView.vue @@ -257,10 +257,14 @@ const loadProject = async () => { if (res.data.status === 'ontology_generated' && !res.data.graph_id) { await startBuildGraph() - } else if (res.data.status === 'graph_building' && res.data.graph_build_task_id) { - currentPhase.value = 1 - startPollingTask(res.data.graph_build_task_id) - startGraphPolling() + } else if (res.data.status === 'graph_building') { + const taskId = res.data.active_task_id || res.data.graph_build_task_id + if (taskId) { + currentPhase.value = 1 + addLog(t('log.reconnectingToTask', { taskId })) + startPollingTask(taskId) + startGraphPolling() + } } else if (res.data.status === 'graph_completed' && res.data.graph_id) { currentPhase.value = 2 await loadGraph(res.data.graph_id) diff --git a/locales/ca.json b/locales/ca.json index cc94779c..e963bfb0 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -596,7 +596,8 @@ "getReportInfoFailed": "Error en obtenir la informació de l'informe: {error}", "enterStep": "Entrant al pas {step}: {name}", "returnToStep": "Tornant al pas {step}: {name}", - "customSimRounds": "Rondes de simulació personalitzades: {rounds} rondes" + "customSimRounds": "Rondes de simulació personalitzades: {rounds} rondes", + "reconnectingToTask": "Reconnectant a la tasca activa {taskId}…" }, "report": { "taskStarted": "Tasca de generació de l'informe iniciada", diff --git a/locales/en.json b/locales/en.json index 7f7eb8de..3b18b8ea 100644 --- a/locales/en.json +++ b/locales/en.json @@ -597,7 +597,8 @@ "getReportInfoFailed": "Failed to get report info: {error}", "enterStep": "Entering Step {step}: {name}", "returnToStep": "Returning to Step {step}: {name}", - "customSimRounds": "Custom simulation rounds: {rounds} rounds" + "customSimRounds": "Custom simulation rounds: {rounds} rounds", + "reconnectingToTask": "Reconnecting to active task {taskId}…" }, "report": { "taskStarted": "Report generation task started", diff --git a/locales/es.json b/locales/es.json index a85fdd9f..fd93b409 100644 --- a/locales/es.json +++ b/locales/es.json @@ -596,7 +596,8 @@ "getReportInfoFailed": "Error al obtener información del informe: {error}", "enterStep": "Entrando al Paso {step}: {name}", "returnToStep": "Volviendo al Paso {step}: {name}", - "customSimRounds": "Rondas de simulación personalizadas: {rounds} rondas" + "customSimRounds": "Rondas de simulación personalizadas: {rounds} rondas", + "reconnectingToTask": "Reconectando a la tarea activa {taskId}…" }, "report": { "taskStarted": "Tarea de generación de informe iniciada", diff --git a/locales/zh.json b/locales/zh.json index a6037cab..32650e66 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -597,7 +597,8 @@ "getReportInfoFailed": "获取报告信息失败: {error}", "enterStep": "进入 Step {step}: {name}", "returnToStep": "返回 Step {step}: {name}", - "customSimRounds": "自定义模拟轮数: {rounds} 轮" + "customSimRounds": "自定义模拟轮数: {rounds} 轮", + "reconnectingToTask": "重新连接到活动任务 {taskId}…" }, "report": { "taskStarted": "报告生成任务开始",