From 9ab3ae67bf83905f1f26798354bbb6acc20cdbb5 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 17:53:02 +0530 Subject: [PATCH 01/15] feat: deep-link success toast to Run History + auto-expand latest run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Quick Deploy success toast now includes a clickable "View in Run History →" link that navigates to /project/job/history?task=, preselecting the job. On arrival, the Run History page auto-expands the most recent run (first row) in addition to any FAILURE rows, so the user immediately sees the deploy they just triggered. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../editor/no-code-model/no-code-model.jsx | 21 +++++++++++++++---- frontend/src/ide/run-history/Runhistory.jsx | 17 ++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index fe7aa9e..4aa1315 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -1829,13 +1829,26 @@ function NoCodeModel({ nodeData }) { ); const envName = selected?.environment_name || "the selected environment"; const jobName = selected?.task_name || ""; + const taskId = quickDeployModal.selectedTaskId; notify({ type: "success", message: "Deploy Triggered", - description: - selectedScope === "job" - ? `Job "${jobName}" is running on "${envName}" (all enabled models). Check Run History for progress.` - : `"${currentModelName}" is running on "${envName}" via job "${jobName}". Check Run History for progress.`, + description: ( + + {selectedScope === "job" + ? `Job "${jobName}" is running on "${envName}" (all enabled models). ` + : `"${currentModelName}" is running on "${envName}" via job "${jobName}". `} + { + e.preventDefault(); + navigate(`/project/job/history?task=${taskId}`); + }} + > + View in Run History → + + + ), }); setRefreshModels(true); setRecentRunsState((prev) => ({ ...prev, fetchedFor: null })); diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx index 2bd50b0..57601fa 100644 --- a/frontend/src/ide/run-history/Runhistory.jsx +++ b/frontend/src/ide/run-history/Runhistory.jsx @@ -208,13 +208,20 @@ const Runhistory = () => { backUpData, ]); - /* ─── auto-expand failed rows on fresh data load (not on filter changes) ─── */ + /* ─── auto-expand on fresh data load ─── */ useEffect(() => { - const failedIds = (backUpData || []) + const ids = []; + const fromDeepLink = searchParams.has("task"); + if (fromDeepLink && backUpData.length > 0) { + ids.push(backUpData[0].id); + } + (backUpData || []) .filter((r) => r.status === "FAILURE" && r.error_message) - .map((r) => r.id); - setExpandedRowKeys(failedIds); - }, [backUpData]); + .forEach((r) => { + if (!ids.includes(r.id)) ids.push(r.id); + }); + setExpandedRowKeys(ids); + }, [backUpData, searchParams]); /* ─── handlers ─── */ const handleJobChange = useCallback( From 349a442355afe3368d30fd89f08a3270ac8d0a23 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 17:58:01 +0530 Subject: [PATCH 02/15] feat: pre-fill create-job form from Quick Deploy 0-candidates CTA When no job covers the current model, clicking "Go to Scheduler" now navigates to /project/job/list?create=1&project=&model=. The Jobs List reads these params: auto-opens the create drawer, and JobDeploy pre-enables the specified model in Model Configuration with the config panel auto-expanded. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ide/editor/no-code-model/no-code-model.jsx | 7 ++++++- frontend/src/ide/scheduler/JobDeploy.jsx | 18 ++++++++++++++++++ frontend/src/ide/scheduler/JobList.jsx | 18 ++++++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index 4aa1315..a900c98 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -1865,7 +1865,12 @@ function NoCodeModel({ nodeData }) { const goToScheduler = () => { setQuickDeployModal((prev) => ({ ...prev, open: false })); - navigate("/project/job/list"); + const params = new URLSearchParams(); + params.set("create", "1"); + if (projectId) params.set("project", projectId); + const modelTitle = nodeData?.node?.title; + if (modelTitle) params.set("model", modelTitle); + navigate(`/project/job/list?${params.toString()}`); }; const runTransformation = (spec) => { diff --git a/frontend/src/ide/scheduler/JobDeploy.jsx b/frontend/src/ide/scheduler/JobDeploy.jsx index cfd111a..4fedcdb 100644 --- a/frontend/src/ide/scheduler/JobDeploy.jsx +++ b/frontend/src/ide/scheduler/JobDeploy.jsx @@ -99,6 +99,7 @@ const JobDeploy = memo(function JobDeploy({ setOpen, selectedJobDeployId, setIsJobListModified, + prefillModel, }) { const [form] = Form.useForm(); const canWrite = checkPermission("JOB_DEPLOYMENT", "can_write"); @@ -223,6 +224,22 @@ const JobDeploy = memo(function JobDeploy({ } }, [selectedProjectId]); + /* ─── pre-fill model from Quick Deploy CTA ─── */ + useEffect(() => { + if (!open || !prefillModel || isEditMode) return; + setModelConfigs((prev) => ({ + ...prev, + [prefillModel]: { + ...prev[prefillModel], + enabled: true, + materialization: prev[prefillModel]?.materialization || "TABLE", + }, + })); + setModelConfigActiveKey((prev) => + prev.includes("model-config") ? prev : ["model-config"] + ); + }, [open, prefillModel, isEditMode]); + /* ─── load existing job when editing ─── */ useEffect(() => { if (!open || !selectedJobDeployId) return; @@ -779,6 +796,7 @@ JobDeploy.propTypes = { PropTypes.number, ]), setIsJobListModified: PropTypes.func.isRequired, + prefillModel: PropTypes.string, }; JobDeploy.displayName = "JobDeploy"; diff --git a/frontend/src/ide/scheduler/JobList.jsx b/frontend/src/ide/scheduler/JobList.jsx index ca0a521..9cc765f 100644 --- a/frontend/src/ide/scheduler/JobList.jsx +++ b/frontend/src/ide/scheduler/JobList.jsx @@ -1,7 +1,7 @@ import { useEffect, useState, useCallback, useMemo } from "react"; import { Alert, Button, Space, Typography, Modal, Pagination } from "antd"; import debounce from "lodash/debounce"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { checkPermission } from "../../common/helpers"; import { useNotificationService } from "../../service/notification-service"; @@ -38,6 +38,8 @@ const JobList = () => { const [openJobDeploy, setOpenJobDeploy] = useState(false); const [selectedJobId, setSelectedJobId] = useState(null); const [searchQuery, setSearchQuery] = useState(""); + const [searchParams, setSearchParams] = useSearchParams(); + const [prefillModel, setPrefillModel] = useState(null); const [filters, setFilters] = useState({ proj: "all", env: "all" }); const { currentPage, @@ -154,9 +156,20 @@ const JobList = () => { }, []); useEffect(() => { - if (!openJobDeploy) setSelectedJobId(null); + if (!openJobDeploy) { + setSelectedJobId(null); + setPrefillModel(null); + } }, [openJobDeploy]); + useEffect(() => { + if (searchParams.get("create") === "1") { + setPrefillModel(searchParams.get("model") || null); + setOpenJobDeploy(true); + setSearchParams({}, { replace: true }); + } + }, [searchParams, setSearchParams]); + const onDelete = async () => { try { await deleteTask(delTaskDetail.projectId, delTaskDetail.taskId); @@ -259,6 +272,7 @@ const JobList = () => { setOpen={setOpenJobDeploy} selectedJobDeployId={selectedJobId} setIsJobListModified={setIsJobListModified} + prefillModel={prefillModel} /> Date: Thu, 16 Apr 2026 18:01:15 +0530 Subject: [PATCH 03/15] feat: promote trigger + scope to real DB columns on TaskRunHistory Previously stored only in kwargs JSON, making server-side filtering impossible. Now first-class nullable CharField columns with DB indexes, written by trigger_scheduled_run alongside kwargs. - Migration 0002 adds trigger (scheduled/manual) and scope (job/model) columns with defaults matching existing behavior. - celery_tasks.py writes both the columns and kwargs (backward compat). - Frontend getRunTriggerScope prefers top-level row.trigger / row.scope (from serializer) and falls back to kwargs for pre-migration rows. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../backend/core/scheduler/celery_tasks.py | 2 + .../0002_taskrunhistory_trigger_scope.py | 43 +++++++++++++++++++ backend/backend/core/scheduler/models.py | 14 ++++++ frontend/src/ide/run-history/Runhistory.jsx | 7 ++- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 backend/backend/core/scheduler/migrations/0002_taskrunhistory_trigger_scope.py diff --git a/backend/backend/core/scheduler/celery_tasks.py b/backend/backend/core/scheduler/celery_tasks.py index add5a2f..ac5812f 100644 --- a/backend/backend/core/scheduler/celery_tasks.py +++ b/backend/backend/core/scheduler/celery_tasks.py @@ -264,6 +264,8 @@ def trigger_scheduled_run( start_time=timezone.now(), user_task_detail=user_task, kwargs=run_kwargs, + trigger=trigger, + scope=scope, ) # ── Mark task as running ────────────────────────────────────────── diff --git a/backend/backend/core/scheduler/migrations/0002_taskrunhistory_trigger_scope.py b/backend/backend/core/scheduler/migrations/0002_taskrunhistory_trigger_scope.py new file mode 100644 index 0000000..aee8c7a --- /dev/null +++ b/backend/backend/core/scheduler/migrations/0002_taskrunhistory_trigger_scope.py @@ -0,0 +1,43 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("job_scheduler", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="taskrunhistory", + name="trigger", + field=models.CharField( + choices=[("scheduled", "Scheduled"), ("manual", "Manual")], + default="scheduled", + help_text="How the run was initiated: cron/interval schedule or manual dispatch.", + max_length=20, + ), + ), + migrations.AddField( + model_name="taskrunhistory", + name="scope", + field=models.CharField( + choices=[("job", "Full job"), ("model", "Single model")], + default="job", + help_text="Whether the run executed all job models or a single model.", + max_length=20, + ), + ), + migrations.AddIndex( + model_name="taskrunhistory", + index=models.Index( + fields=["trigger"], name="job_schedul_trigger_idx" + ), + ), + migrations.AddIndex( + model_name="taskrunhistory", + index=models.Index( + fields=["scope"], name="job_schedul_scope_idx" + ), + ), + ] diff --git a/backend/backend/core/scheduler/models.py b/backend/backend/core/scheduler/models.py index a2130d7..799ac84 100644 --- a/backend/backend/core/scheduler/models.py +++ b/backend/backend/core/scheduler/models.py @@ -132,6 +132,18 @@ class TaskRunHistory(DefaultOrganizationMixin, BaseModel): kwargs = models.JSONField(blank=True, null=True) result = models.JSONField(blank=True, null=True) error_message = models.TextField(blank=True, null=True) + trigger = models.CharField( + max_length=20, + choices=[("scheduled", "Scheduled"), ("manual", "Manual")], + default="scheduled", + help_text="How the run was initiated: cron/interval schedule or manual dispatch.", + ) + scope = models.CharField( + max_length=20, + choices=[("job", "Full job"), ("model", "Single model")], + default="job", + help_text="Whether the run executed all job models or a single model.", + ) user_task_detail = models.ForeignKey( UserTaskDetails, @@ -154,6 +166,8 @@ class Meta: models.Index( fields=["user_task_detail"], name="job_schedul_user_ta_5cd43a_idx" ), + models.Index(fields=["trigger"], name="job_schedul_trigger_idx"), + models.Index(fields=["scope"], name="job_schedul_scope_idx"), ] def __str__(self): diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx index 57601fa..342cfb1 100644 --- a/frontend/src/ide/run-history/Runhistory.jsx +++ b/frontend/src/ide/run-history/Runhistory.jsx @@ -73,9 +73,12 @@ const getRunTriggerScope = (row) => { const kw = row?.kwargs || {}; const legacyQuick = kw.source === "quick_deploy"; const models = kw.models_override || []; - const trigger = kw.trigger || (legacyQuick ? "manual" : "scheduled"); + const trigger = + row?.trigger || kw.trigger || (legacyQuick ? "manual" : "scheduled"); const scope = - kw.scope || (models.length > 0 || legacyQuick ? "model" : "job"); + row?.scope || + kw.scope || + (models.length > 0 || legacyQuick ? "model" : "job"); return { trigger, scope, models }; }; From 04db1affe8f6b4c6ed00f2d1eb5dd587e07c8c9e Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 18:02:56 +0530 Subject: [PATCH 04/15] feat: server-side Run History filtering by trigger, scope, status Replaces client-side filtering with server-side query params on the task_run_history endpoint. Filter changes now trigger a fresh API call with ?trigger=manual&scope=model&status=FAILURE, so results are accurate across all pages (previously client-side filtering only worked on the visible page). Backend accepts optional trigger, scope, status query params and applies them as Django ORM filters against the new DB columns from the previous migration. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/core/scheduler/views.py | 14 ++++++++- frontend/src/ide/run-history/Runhistory.jsx | 35 ++++++++++----------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/backend/backend/core/scheduler/views.py b/backend/backend/core/scheduler/views.py index c0c341e..0d3bba9 100644 --- a/backend/backend/core/scheduler/views.py +++ b/backend/backend/core/scheduler/views.py @@ -586,7 +586,19 @@ def task_run_history(request, project_id, user_task_id): if _is_valid_project_id(project_id): query["project__project_uuid"] = project_id task = UserTaskDetails.objects.get(**query) - runs = TaskRunHistory.objects.filter(user_task_detail=task).order_by("-start_time") + runs = TaskRunHistory.objects.filter(user_task_detail=task) + + trigger_filter = request.GET.get("trigger") + scope_filter = request.GET.get("scope") + status_filter = request.GET.get("status") + if trigger_filter: + runs = runs.filter(trigger=trigger_filter) + if scope_filter: + runs = runs.filter(scope=scope_filter) + if status_filter: + runs = runs.filter(status=status_filter) + + runs = runs.order_by("-start_time") total = runs.count() offset = (page - 1) * limit diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx index 342cfb1..5b6a6df 100644 --- a/frontend/src/ide/run-history/Runhistory.jsx +++ b/frontend/src/ide/run-history/Runhistory.jsx @@ -117,15 +117,19 @@ const Runhistory = () => { /* ─── API calls ─── */ const getRunHistoryList = useCallback( - async (Id, page = currentPage, limit = pageSize) => { + async (Id, page = currentPage, limit = pageSize, filters = {}) => { setLoading(true); try { + const params = { page, limit }; + if (filters.status) params.status = filters.status; + if (filters.trigger) params.trigger = filters.trigger; + if (filters.scope) params.scope = filters.scope; const res = await axios({ method: "GET", url: `/api/v1/visitran/${ selectedOrgId || "default_org" }/project/_all/jobs/run-history/${Id}`, - params: { page, limit }, + params, }); const { page_items, total_items, current_page } = res.data.data; setTotalCount(total_items); @@ -187,28 +191,21 @@ const Runhistory = () => { getJobList(); }, []); - /* ─── client-side status + trigger + scope filters ─── */ + /* ─── server-side filtering: refetch when filters change ─── */ useEffect(() => { - let filtered = backUpData; - if (filterQueries.status) { - filtered = filtered.filter((el) => el.status === filterQueries.status); - } - if (filterQueries.trigger) { - filtered = filtered.filter( - (el) => getRunTriggerScope(el).trigger === filterQueries.trigger - ); - } - if (filterQueries.scope) { - filtered = filtered.filter( - (el) => getRunTriggerScope(el).scope === filterQueries.scope - ); - } - setJobHistoryData(filtered); + if (!filterQueries.job) return; + getRunHistoryList(filterQueries.job, 1, pageSize, { + status: filterQueries.status, + trigger: filterQueries.trigger, + scope: filterQueries.scope, + }); }, [ filterQueries.status, filterQueries.trigger, filterQueries.scope, - backUpData, + filterQueries.job, + getRunHistoryList, + pageSize, ]); /* ─── auto-expand on fresh data load ─── */ From 0ff6aa070245a5b88defb73e6fe2c4f77806646d Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 18:07:16 +0530 Subject: [PATCH 05/15] feat: live deploy progress polling on Quick Deploy button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After dispatching a deploy, the Quick Deploy button flips to "Deploying…" with a spinner and polls the latest run status every 5s. On terminal state (SUCCESS/FAILURE/REVOKED): - Clears the polling interval - Shows a completion toast with status + deep-link to Run History - Refreshes the explorer (status badges) and recent-runs cache Polling auto-cleans on component unmount. The button returns to its normal state when the run finishes or the component unmounts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../editor/no-code-model/no-code-model.jsx | 65 +++++++++++++++++-- frontend/src/ide/scheduler/service.js | 10 +++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index a900c98..24519a2 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -258,6 +258,7 @@ function NoCodeModel({ nodeData }) { runTaskForModel, runTask, listRecentRunsForModel, + getLatestRunStatus, } = useJobService(); const [quickDeployModal, setQuickDeployModal] = useState({ @@ -272,8 +273,10 @@ function NoCodeModel({ nodeData }) { const [recentRunsState, setRecentRunsState] = useState({ loading: false, runs: [], - fetchedFor: null, // model name the current runs are for + fetchedFor: null, }); + const [deployPolling, setDeployPolling] = useState(null); + const pollingRef = useRef(null); const modelName = nodeData?.node?.title || @@ -1850,8 +1853,7 @@ function NoCodeModel({ nodeData }) { ), }); - setRefreshModels(true); - setRecentRunsState((prev) => ({ ...prev, fetchedFor: null })); + startDeployPolling(quickDeployModal.selectedTaskId); setQuickDeployModal((prev) => ({ ...prev, open: false, @@ -1863,6 +1865,58 @@ function NoCodeModel({ nodeData }) { } }; + const startDeployPolling = (taskId) => { + if (pollingRef.current) clearInterval(pollingRef.current); + setDeployPolling({ taskId, status: "STARTED" }); + pollingRef.current = setInterval(async () => { + try { + const run = await getLatestRunStatus(projectId, taskId); + if (!run) return; + const terminal = ["SUCCESS", "FAILURE", "REVOKED"].includes(run.status); + if (terminal) { + clearInterval(pollingRef.current); + pollingRef.current = null; + setDeployPolling(null); + setRefreshModels(true); + setRecentRunsState((prev) => ({ ...prev, fetchedFor: null })); + notify({ + type: run.status === "SUCCESS" ? "success" : "error", + message: + run.status === "SUCCESS" ? "Deploy Completed" : "Deploy Failed", + description: ( + + {run.status === "SUCCESS" + ? "Model deployed successfully." + : run.error_message || "Check Run History for details."}{" "} + { + e.preventDefault(); + navigate(`/project/job/history?task=${taskId}`); + }} + > + View in Run History → + + + ), + }); + } else { + setDeployPolling((prev) => + prev ? { ...prev, status: run.status } : null + ); + } + } catch { + // Silently retry on next interval + } + }, 5000); + }; + + useEffect(() => { + return () => { + if (pollingRef.current) clearInterval(pollingRef.current); + }; + }, []); + const goToScheduler = () => { setQuickDeployModal((prev) => ({ ...prev, open: false })); const params = new URLSearchParams(); @@ -2841,9 +2895,10 @@ function NoCodeModel({ nodeData }) { !can_write || !nodeData?.node?.title } - icon={} + loading={!!deployPolling} + icon={!deployPolling ? : undefined} > - Quick Deploy + {deployPolling ? "Deploying…" : "Quick Deploy"} { + const url = `${jobsUrl(projId)}/run-history/${taskId}`; + const response = await axiosPrivate.get(url, { + params: { page: 1, limit: 1 }, + }); + const runs = response.data?.data?.page_items?.run_history || []; + return runs.length > 0 ? runs[0] : null; + }; + const listRecentRunsForModel = async (projId, modelName, limit = 5) => { const url = `${jobsUrl( projId @@ -144,6 +153,7 @@ export function useJobService() { runTaskForModel, listDeployCandidates, listRecentRunsForModel, + getLatestRunStatus, getProjects, getEnvironments, getProjectModels, From b607842e30d66dc2764e9e91b18e42b785f3b51a Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 18:10:24 +0530 Subject: [PATCH 06/15] feat: capture runtime metrics from BASE_RESULT into TaskRunHistory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After DAG execution (success or failure), trigger_scheduled_run now serializes BASE_RESULT into run.result as JSON with per-model status/end_status and aggregate passed/failed counts. Frontend insights panel renders a metrics bar when result is present: "N models attempted · X passed · Y failed" plus per-model breakdown. Falls back gracefully to scope/models display for older runs without result data. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../backend/core/scheduler/celery_tasks.py | 42 ++++++++++++++++++- frontend/src/ide/run-history/Runhistory.jsx | 31 ++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/backend/backend/core/scheduler/celery_tasks.py b/backend/backend/core/scheduler/celery_tasks.py index ac5812f..a0dc7cf 100644 --- a/backend/backend/core/scheduler/celery_tasks.py +++ b/backend/backend/core/scheduler/celery_tasks.py @@ -323,11 +323,31 @@ def trigger_scheduled_run( else: app_context.execute_visitran_run_command(environment_id=environment_id) + # ── Capture execution metrics from BASE_RESULT ────────────── + try: + from visitran.events.printer import BASE_RESULT + run.result = { + "models": [ + { + "name": r.node_name, + "status": r.status, + "end_status": r.end_status, + "sequence": r.sequence_num, + } + for r in BASE_RESULT + ], + "total": len(BASE_RESULT), + "passed": sum(1 for r in BASE_RESULT if r.end_status == "OK"), + "failed": sum(1 for r in BASE_RESULT if r.end_status == "FAIL"), + } + except Exception: + logger.debug("Could not capture BASE_RESULT metrics", exc_info=True) + # ── Mark success ────────────────────────────────────────────── success = True run.status = "SUCCESS" run.end_time = timezone.now() - run.save(update_fields=["status", "end_time"]) + run.save(update_fields=["status", "end_time", "result"]) user_task.status = TaskStatus.SUCCESS user_task.task_completion_time = run.end_time @@ -382,10 +402,28 @@ def trigger_scheduled_run( def _mark_failure(run: TaskRunHistory, user_task: UserTaskDetails, error_msg: str): """Helper to mark a run and its parent task as failed.""" + try: + from visitran.events.printer import BASE_RESULT + run.result = { + "models": [ + { + "name": r.node_name, + "status": r.status, + "end_status": r.end_status, + "sequence": r.sequence_num, + } + for r in BASE_RESULT + ], + "total": len(BASE_RESULT), + "passed": sum(1 for r in BASE_RESULT if r.end_status == "OK"), + "failed": sum(1 for r in BASE_RESULT if r.end_status == "FAIL"), + } + except Exception: + pass run.status = "FAILURE" run.end_time = timezone.now() run.error_message = error_msg - run.save(update_fields=["status", "end_time", "error_message"]) + run.save(update_fields=["status", "end_time", "error_message", "result"]) user_task.status = TaskStatus.FAILED user_task.task_completion_time = run.end_time diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx index 5b6a6df..0327fed 100644 --- a/frontend/src/ide/run-history/Runhistory.jsx +++ b/frontend/src/ide/run-history/Runhistory.jsx @@ -597,6 +597,37 @@ const Runhistory = () => { : "No model configuration recorded for this run."} + {record.result && ( +
+ + {record.result.total || 0} models + attempted + + + {record.result.passed || 0} passed + + + {record.result.failed || 0} failed + + {record.result.models?.length > 0 && ( + + {record.result.models + .map((m) => `${m.name} (${m.end_status})`) + .join(", ")} + + )} +
+ )} {isFailure && record.error_message && ( Date: Thu, 16 Apr 2026 18:18:50 +0530 Subject: [PATCH 07/15] =?UTF-8?q?fix:=20toast=20JSX=20links=20not=20render?= =?UTF-8?q?ing=20=E2=80=94=20add=20renderMarkdown:=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notify service defaults to renderMarkdown: true, which wraps description in ReactMarkdown. When description is JSX (our link), ReactMarkdown stringifies it via JSON.stringify, rendering as raw text instead of a clickable link. Added renderMarkdown: false to both the dispatch toast and the polling-completion toast. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/ide/editor/no-code-model/no-code-model.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index 24519a2..080dfdf 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -1836,6 +1836,7 @@ function NoCodeModel({ nodeData }) { notify({ type: "success", message: "Deploy Triggered", + renderMarkdown: false, description: ( {selectedScope === "job" @@ -1883,6 +1884,7 @@ function NoCodeModel({ nodeData }) { type: run.status === "SUCCESS" ? "success" : "error", message: run.status === "SUCCESS" ? "Deploy Completed" : "Deploy Failed", + renderMarkdown: false, description: ( {run.status === "SUCCESS" From e8465cb18cffe996e2652cd4b65873a7e97d62ea Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 18:29:01 +0530 Subject: [PATCH 08/15] fix: hide metrics bar when result has no model data Old runs have result as {} or with total=0. Guard with record.result?.total > 0 so the metrics bar only renders when there's actual execution data. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/ide/run-history/Runhistory.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx index 0327fed..0ad76ae 100644 --- a/frontend/src/ide/run-history/Runhistory.jsx +++ b/frontend/src/ide/run-history/Runhistory.jsx @@ -597,7 +597,7 @@ const Runhistory = () => { : "No model configuration recorded for this run."} - {record.result && ( + {record.result?.total > 0 && (
Date: Thu, 16 Apr 2026 18:43:00 +0530 Subject: [PATCH 09/15] fix: extract clean model name from class repr in run metrics BASE_RESULT.node_name stores str(cls) which renders as . Extract the module name (second-to-last dotted segment) so metrics show "mdoela" instead of the full class repr. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/core/scheduler/celery_tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/backend/core/scheduler/celery_tasks.py b/backend/backend/core/scheduler/celery_tasks.py index a0dc7cf..aeceb1b 100644 --- a/backend/backend/core/scheduler/celery_tasks.py +++ b/backend/backend/core/scheduler/celery_tasks.py @@ -329,7 +329,7 @@ def trigger_scheduled_run( run.result = { "models": [ { - "name": r.node_name, + "name": r.node_name.split("'")[1].split(".")[-2] if "'" in r.node_name else r.node_name, "status": r.status, "end_status": r.end_status, "sequence": r.sequence_num, @@ -407,7 +407,7 @@ def _mark_failure(run: TaskRunHistory, user_task: UserTaskDetails, error_msg: st run.result = { "models": [ { - "name": r.node_name, + "name": r.node_name.split("'")[1].split(".")[-2] if "'" in r.node_name else r.node_name, "status": r.status, "end_status": r.end_status, "sequence": r.sequence_num, From 40ddbbcc62d983217bed59d8782538b7560223e1 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 18:45:24 +0530 Subject: [PATCH 10/15] fix: use class name for metrics instead of module name A model file can define multiple classes (e.g. SourceMdoela + Mdoela) in the same module. Using [-2] (module name) made them indistinguishable. Switch to [-1] (class name) so the metrics display shows "SourceMdoela (OK), Mdoela (OK)" instead of "mdoela (OK), mdoela (OK)". Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/core/scheduler/celery_tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/backend/core/scheduler/celery_tasks.py b/backend/backend/core/scheduler/celery_tasks.py index aeceb1b..7c6a8d1 100644 --- a/backend/backend/core/scheduler/celery_tasks.py +++ b/backend/backend/core/scheduler/celery_tasks.py @@ -329,7 +329,7 @@ def trigger_scheduled_run( run.result = { "models": [ { - "name": r.node_name.split("'")[1].split(".")[-2] if "'" in r.node_name else r.node_name, + "name": r.node_name.split("'")[1].split(".")[-1] if "'" in r.node_name else r.node_name, "status": r.status, "end_status": r.end_status, "sequence": r.sequence_num, @@ -407,7 +407,7 @@ def _mark_failure(run: TaskRunHistory, user_task: UserTaskDetails, error_msg: st run.result = { "models": [ { - "name": r.node_name.split("'")[1].split(".")[-2] if "'" in r.node_name else r.node_name, + "name": r.node_name.split("'")[1].split(".")[-1] if "'" in r.node_name else r.node_name, "status": r.status, "end_status": r.end_status, "sequence": r.sequence_num, From 2eedc37e091cf9523837f0fdf1c8e5d9dcbae8df Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 18:48:32 +0530 Subject: [PATCH 11/15] fix: filter out Source classes from run metrics No-code models generate a *Source class (e.g. MdoelaSource) for DAG dependency resolution alongside the user's actual model class. Both execute as DAG nodes and appear in BASE_RESULT, but users only care about their own models. Filter out classes ending with "Source" from the metrics serialization so the count and per-model list reflect user-created models only. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../backend/core/scheduler/celery_tasks.py | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/backend/backend/core/scheduler/celery_tasks.py b/backend/backend/core/scheduler/celery_tasks.py index 7c6a8d1..96f6d15 100644 --- a/backend/backend/core/scheduler/celery_tasks.py +++ b/backend/backend/core/scheduler/celery_tasks.py @@ -326,19 +326,29 @@ def trigger_scheduled_run( # ── Capture execution metrics from BASE_RESULT ────────────── try: from visitran.events.printer import BASE_RESULT + + def _clean_name(raw): + if "'" in raw: + return raw.split("'")[1].split(".")[-1] + return raw + + user_results = [ + r for r in BASE_RESULT + if not _clean_name(r.node_name).endswith("Source") + ] run.result = { "models": [ { - "name": r.node_name.split("'")[1].split(".")[-1] if "'" in r.node_name else r.node_name, + "name": _clean_name(r.node_name), "status": r.status, "end_status": r.end_status, "sequence": r.sequence_num, } - for r in BASE_RESULT + for r in user_results ], - "total": len(BASE_RESULT), - "passed": sum(1 for r in BASE_RESULT if r.end_status == "OK"), - "failed": sum(1 for r in BASE_RESULT if r.end_status == "FAIL"), + "total": len(user_results), + "passed": sum(1 for r in user_results if r.end_status == "OK"), + "failed": sum(1 for r in user_results if r.end_status == "FAIL"), } except Exception: logger.debug("Could not capture BASE_RESULT metrics", exc_info=True) @@ -404,19 +414,26 @@ def _mark_failure(run: TaskRunHistory, user_task: UserTaskDetails, error_msg: st """Helper to mark a run and its parent task as failed.""" try: from visitran.events.printer import BASE_RESULT + + def _clean(raw): + return raw.split("'")[1].split(".")[-1] if "'" in raw else raw + + user_results = [ + r for r in BASE_RESULT if not _clean(r.node_name).endswith("Source") + ] run.result = { "models": [ { - "name": r.node_name.split("'")[1].split(".")[-1] if "'" in r.node_name else r.node_name, + "name": _clean(r.node_name), "status": r.status, "end_status": r.end_status, "sequence": r.sequence_num, } - for r in BASE_RESULT + for r in user_results ], - "total": len(BASE_RESULT), - "passed": sum(1 for r in BASE_RESULT if r.end_status == "OK"), - "failed": sum(1 for r in BASE_RESULT if r.end_status == "FAIL"), + "total": len(user_results), + "passed": sum(1 for r in user_results if r.end_status == "OK"), + "failed": sum(1 for r in user_results if r.end_status == "FAIL"), } except Exception: pass From bede6a9f686806c83c6156d768da1ffdb92e83df Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 18:51:13 +0530 Subject: [PATCH 12/15] fix: Source classes start with Source, not end with it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SourceMdoela, DevPaymentsSource — the sample projects use both conventions. The generated no-code models use the prefix pattern (SourceX). Changed endswith to startswith to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/core/scheduler/celery_tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/backend/core/scheduler/celery_tasks.py b/backend/backend/core/scheduler/celery_tasks.py index 96f6d15..04bcf69 100644 --- a/backend/backend/core/scheduler/celery_tasks.py +++ b/backend/backend/core/scheduler/celery_tasks.py @@ -334,7 +334,7 @@ def _clean_name(raw): user_results = [ r for r in BASE_RESULT - if not _clean_name(r.node_name).endswith("Source") + if not _clean_name(r.node_name).startswith("Source") ] run.result = { "models": [ @@ -419,7 +419,7 @@ def _clean(raw): return raw.split("'")[1].split(".")[-1] if "'" in raw else raw user_results = [ - r for r in BASE_RESULT if not _clean(r.node_name).endswith("Source") + r for r in BASE_RESULT if not _clean(r.node_name).startswith("Source") ] run.result = { "models": [ From 3c44484a1eeb341488db73341ddca2050276c814 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Thu, 16 Apr 2026 20:19:23 +0530 Subject: [PATCH 13/15] =?UTF-8?q?fix:=20address=20PR=20#64=20review=20?= =?UTF-8?q?=E2=80=94=20pagination=20filters,=20stale=20global,=20closure?= =?UTF-8?q?=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pass active filters through handlePagination and handleRefresh (P1) - Snapshot-then-clear BASE_RESULT to prevent stale metrics across worker reuse (P1) - Fix handleRefresh stale closure deps (P2) - Forward project URL param from goToScheduler to JobDeploy (P2) - Prefer DB columns over kwargs for trigger/scope in list_recent_runs_for_model (P2) - Sanitize taskId with encodeURIComponent in toast deep-links (CodeQL) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../backend/core/scheduler/celery_tasks.py | 23 ++++++++++++++++++- backend/backend/core/scheduler/views.py | 8 +++---- .../editor/no-code-model/no-code-model.jsx | 6 ++--- frontend/src/ide/run-history/Runhistory.jsx | 16 +++++++++---- frontend/src/ide/scheduler/JobDeploy.jsx | 9 ++++++++ frontend/src/ide/scheduler/JobList.jsx | 4 ++++ 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/backend/backend/core/scheduler/celery_tasks.py b/backend/backend/core/scheduler/celery_tasks.py index 04bcf69..d794df6 100644 --- a/backend/backend/core/scheduler/celery_tasks.py +++ b/backend/backend/core/scheduler/celery_tasks.py @@ -147,6 +147,19 @@ def _send_notification(user_task: UserTaskDetails, run: TaskRunHistory, success: _send_slack_notification(user_task, run, success) +# --------------------------------------------------------------------------- +# BASE_RESULT cleanup helper +# --------------------------------------------------------------------------- + +def _clear_base_result(): + """Clear the module-level BASE_RESULT global to prevent stale data across worker reuse.""" + try: + from visitran.events.printer import BASE_RESULT + BASE_RESULT.clear() + except Exception: + pass + + # --------------------------------------------------------------------------- # Job chaining helper # --------------------------------------------------------------------------- @@ -327,13 +340,18 @@ def trigger_scheduled_run( try: from visitran.events.printer import BASE_RESULT + # Snapshot and immediately clear the global to prevent stale + # data leaking into a subsequent run on the same worker process. + results_snapshot = list(BASE_RESULT) + BASE_RESULT.clear() + def _clean_name(raw): if "'" in raw: return raw.split("'")[1].split(".")[-1] return raw user_results = [ - r for r in BASE_RESULT + r for r in results_snapshot if not _clean_name(r.node_name).startswith("Source") ] run.result = { @@ -351,6 +369,7 @@ def _clean_name(raw): "failed": sum(1 for r in user_results if r.end_status == "FAIL"), } except Exception: + _clear_base_result() logger.debug("Could not capture BASE_RESULT metrics", exc_info=True) # ── Mark success ────────────────────────────────────────────── @@ -367,11 +386,13 @@ def _clean_name(raw): except (_RunTimeout, SoftTimeLimitExceeded) as exc: error_msg = str(exc) if str(exc) else f"Job exceeded timeout of {timeout}s" logger.warning("Job %s timed out: %s", user_task.task_name, error_msg) + _clear_base_result() _mark_failure(run, user_task, error_msg) except Exception as exc: error_msg = str(exc) logger.exception("Job %s failed: %s", user_task.task_name, error_msg) + _clear_base_result() _mark_failure(run, user_task, error_msg) # ── Retry logic ─────────────────────────────────────────────────── diff --git a/backend/backend/core/scheduler/views.py b/backend/backend/core/scheduler/views.py index 0d3bba9..6fcd35b 100644 --- a/backend/backend/core/scheduler/views.py +++ b/backend/backend/core/scheduler/views.py @@ -759,13 +759,13 @@ def list_recent_runs_for_model(request, project_id, model_name): env = task.environment kwargs = run.kwargs or {} models_override = kwargs.get("models_override") or [] - # Back-compat: rows written before the trigger/scope split only - # carried kwargs.source=="quick_deploy" as their manual-model marker. + # Prefer first-class DB columns; fall back to kwargs for rows + # written before the trigger/scope migration. legacy_source = kwargs.get("source") - trigger = kwargs.get("trigger") or ( + trigger = run.trigger or kwargs.get("trigger") or ( "manual" if legacy_source == "quick_deploy" else "scheduled" ) - scope = kwargs.get("scope") or ( + scope = run.scope or kwargs.get("scope") or ( "model" if models_override or legacy_source == "quick_deploy" else "job" ) data.append({ diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index 080dfdf..5436ebd 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -1832,7 +1832,7 @@ function NoCodeModel({ nodeData }) { ); const envName = selected?.environment_name || "the selected environment"; const jobName = selected?.task_name || ""; - const taskId = quickDeployModal.selectedTaskId; + const taskId = encodeURIComponent(quickDeployModal.selectedTaskId); notify({ type: "success", message: "Deploy Triggered", @@ -1891,10 +1891,10 @@ function NoCodeModel({ nodeData }) { ? "Model deployed successfully." : run.error_message || "Check Run History for details."}{" "} { e.preventDefault(); - navigate(`/project/job/history?task=${taskId}`); + navigate(`/project/job/history?task=${encodeURIComponent(taskId)}`); }} > View in Run History → diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx index 0ad76ae..1e940ef 100644 --- a/frontend/src/ide/run-history/Runhistory.jsx +++ b/frontend/src/ide/run-history/Runhistory.jsx @@ -255,19 +255,27 @@ const Runhistory = () => { const handleRefresh = useCallback(() => { if (filterQueries.job) { - getRunHistoryList(filterQueries.job); + getRunHistoryList(filterQueries.job, currentPage, pageSize, { + status: filterQueries.status, + trigger: filterQueries.trigger, + scope: filterQueries.scope, + }); } - }, [filterQueries.job]); + }, [filterQueries, currentPage, pageSize, getRunHistoryList]); const handlePagination = useCallback( (newPage, newPageSize) => { if (currentPage !== newPage || pageSize !== newPageSize) { setCurrentPage(newPage); setPageSize(newPageSize); - getRunHistoryList(envInfo.id, newPage, newPageSize); + getRunHistoryList(envInfo.id, newPage, newPageSize, { + status: filterQueries.status, + trigger: filterQueries.trigger, + scope: filterQueries.scope, + }); } }, - [currentPage, pageSize, envInfo.id] + [currentPage, pageSize, envInfo.id, filterQueries, getRunHistoryList] ); const handleExpand = useCallback((expanded, record) => { diff --git a/frontend/src/ide/scheduler/JobDeploy.jsx b/frontend/src/ide/scheduler/JobDeploy.jsx index 4fedcdb..93d7d9e 100644 --- a/frontend/src/ide/scheduler/JobDeploy.jsx +++ b/frontend/src/ide/scheduler/JobDeploy.jsx @@ -100,6 +100,7 @@ const JobDeploy = memo(function JobDeploy({ selectedJobDeployId, setIsJobListModified, prefillModel, + prefillProject, }) { const [form] = Form.useForm(); const canWrite = checkPermission("JOB_DEPLOYMENT", "can_write"); @@ -224,6 +225,13 @@ const JobDeploy = memo(function JobDeploy({ } }, [selectedProjectId]); + /* ─── pre-fill project from Quick Deploy CTA ─── */ + useEffect(() => { + if (!open || !prefillProject || isEditMode) return; + form.setFieldsValue({ project: prefillProject }); + setSelectedProjectId(prefillProject); + }, [open, prefillProject, isEditMode, form]); + /* ─── pre-fill model from Quick Deploy CTA ─── */ useEffect(() => { if (!open || !prefillModel || isEditMode) return; @@ -797,6 +805,7 @@ JobDeploy.propTypes = { ]), setIsJobListModified: PropTypes.func.isRequired, prefillModel: PropTypes.string, + prefillProject: PropTypes.string, }; JobDeploy.displayName = "JobDeploy"; diff --git a/frontend/src/ide/scheduler/JobList.jsx b/frontend/src/ide/scheduler/JobList.jsx index 9cc765f..7bca882 100644 --- a/frontend/src/ide/scheduler/JobList.jsx +++ b/frontend/src/ide/scheduler/JobList.jsx @@ -40,6 +40,7 @@ const JobList = () => { const [searchQuery, setSearchQuery] = useState(""); const [searchParams, setSearchParams] = useSearchParams(); const [prefillModel, setPrefillModel] = useState(null); + const [prefillProject, setPrefillProject] = useState(null); const [filters, setFilters] = useState({ proj: "all", env: "all" }); const { currentPage, @@ -159,12 +160,14 @@ const JobList = () => { if (!openJobDeploy) { setSelectedJobId(null); setPrefillModel(null); + setPrefillProject(null); } }, [openJobDeploy]); useEffect(() => { if (searchParams.get("create") === "1") { setPrefillModel(searchParams.get("model") || null); + setPrefillProject(searchParams.get("project") || null); setOpenJobDeploy(true); setSearchParams({}, { replace: true }); } @@ -273,6 +276,7 @@ const JobList = () => { selectedJobDeployId={selectedJobId} setIsJobListModified={setIsJobListModified} prefillModel={prefillModel} + prefillProject={prefillProject} /> Date: Thu, 16 Apr 2026 21:48:36 +0530 Subject: [PATCH 14/15] fix: prettier formatting for toast deep-link JSX Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/ide/editor/no-code-model/no-code-model.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index 5436ebd..ee73807 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -1891,10 +1891,14 @@ function NoCodeModel({ nodeData }) { ? "Model deployed successfully." : run.error_message || "Check Run History for details."}{" "} { e.preventDefault(); - navigate(`/project/job/history?task=${encodeURIComponent(taskId)}`); + navigate( + `/project/job/history?task=${encodeURIComponent(taskId)}` + ); }} > View in Run History → From 4b223a1667bf75c13ae575030fa492c115fb2e54 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Fri, 17 Apr 2026 10:57:43 +0530 Subject: [PATCH 15/15] =?UTF-8?q?fix:=20address=20Greptile=20review=20?= =?UTF-8?q?=E2=80=94=20BASE=5FRESULT=20ordering=20+=20pagination=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1: _mark_failure was called after _clear_base_result(), so failure metrics were always empty. Swapped order: capture metrics first via _mark_failure, then clear the global. P1: getRunHistoryList had currentPage/pageSize in useCallback deps, causing infinite re-creation on pagination. Removed — they're passed as explicit arguments, not captured from closure. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/core/scheduler/celery_tasks.py | 4 ++-- frontend/src/ide/run-history/Runhistory.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/backend/core/scheduler/celery_tasks.py b/backend/backend/core/scheduler/celery_tasks.py index d794df6..1ce72d8 100644 --- a/backend/backend/core/scheduler/celery_tasks.py +++ b/backend/backend/core/scheduler/celery_tasks.py @@ -386,14 +386,14 @@ def _clean_name(raw): except (_RunTimeout, SoftTimeLimitExceeded) as exc: error_msg = str(exc) if str(exc) else f"Job exceeded timeout of {timeout}s" logger.warning("Job %s timed out: %s", user_task.task_name, error_msg) - _clear_base_result() _mark_failure(run, user_task, error_msg) + _clear_base_result() except Exception as exc: error_msg = str(exc) logger.exception("Job %s failed: %s", user_task.task_name, error_msg) - _clear_base_result() _mark_failure(run, user_task, error_msg) + _clear_base_result() # ── Retry logic ─────────────────────────────────────────────────── if not success and user_task.max_retries > 0 and retry_num < user_task.max_retries: diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx index 1e940ef..d4c5b60 100644 --- a/frontend/src/ide/run-history/Runhistory.jsx +++ b/frontend/src/ide/run-history/Runhistory.jsx @@ -144,7 +144,7 @@ const Runhistory = () => { setLoading(false); } }, - [axios, selectedOrgId, currentPage, pageSize, notify] + [axios, selectedOrgId, notify] ); const getJobList = async () => {