feat(recovery): persist active_task_id to project.json for browser-refresh reconnection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-04-26 14:54:44 +00:00
parent 656a3d7d5c
commit 117eabf607
8 changed files with 57 additions and 10 deletions

View File

@ -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(

View File

@ -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'),
)

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -597,7 +597,8 @@
"getReportInfoFailed": "获取报告信息失败: {error}",
"enterStep": "进入 Step {step}: {name}",
"returnToStep": "返回 Step {step}: {name}",
"customSimRounds": "自定义模拟轮数: {rounds} 轮"
"customSimRounds": "自定义模拟轮数: {rounds} 轮",
"reconnectingToTask": "重新连接到活动任务 {taskId}…"
},
"report": {
"taskStarted": "报告生成任务开始",