diff --git a/backend/backend/core/scheduler/views.py b/backend/backend/core/scheduler/views.py
index 122ff150..c0c341e1 100644
--- a/backend/backend/core/scheduler/views.py
+++ b/backend/backend/core/scheduler/views.py
@@ -20,6 +20,21 @@
logger = logging.getLogger(__name__)
+
+def _compute_next_run_time(periodic, last_run_at):
+ """Derive the next run time from a PeriodicTask's schedule."""
+ if not periodic or not periodic.enabled:
+ return None
+ try:
+ schedule = periodic.schedule
+ reference = last_run_at or periodic.last_run_at or timezone.now()
+ remaining = schedule.remaining_estimate(reference)
+ return timezone.now() + remaining
+ except Exception:
+ logger.debug("Failed to compute next_run_time for %s", periodic, exc_info=True)
+ return None
+
+
def _is_valid_project_id(project_id):
"""Check if project_id is a real UUID (not a placeholder like '_all' or 'all')."""
try:
@@ -164,6 +179,9 @@ def _serialize_task(task):
"task_status": task.status,
"task_run_time": task.task_run_time,
"task_completion_time": task.task_completion_time,
+ "next_run_time": task.next_run_time or _compute_next_run_time(
+ periodic, task.task_run_time
+ ),
"task_type": task_type,
"description": task.description,
"environment": {
diff --git a/frontend/src/common/helpers.js b/frontend/src/common/helpers.js
index 83a1afe6..8d00ccbe 100644
--- a/frontend/src/common/helpers.js
+++ b/frontend/src/common/helpers.js
@@ -354,15 +354,31 @@ const getRelativeTime = (dateString) => {
const now = new Date();
const then = new Date(dateString);
const diffMs = now - then;
- const diffMins = Math.floor(diffMs / 60000);
- if (diffMins < 1) return "just now";
- if (diffMins < 60) return `${diffMins}m ago`;
- const diffHrs = Math.floor(diffMins / 60);
- if (diffHrs < 24) return `${diffHrs}h ago`;
- const diffDays = Math.floor(diffHrs / 24);
- if (diffDays < 30) return `${diffDays}d ago`;
- const diffMonths = Math.floor(diffDays / 30);
- return `${diffMonths}mo ago`;
+ const isPast = diffMs >= 0;
+ const absMs = Math.abs(diffMs);
+ const mins = Math.floor(absMs / 60000);
+ const fmt = (n, unit) => (isPast ? `${n}${unit} ago` : `in ${n}${unit}`);
+ if (mins < 1) return isPast ? "just now" : "in a moment";
+ if (mins < 60) return fmt(mins, "m");
+ const hrs = Math.floor(mins / 60);
+ if (hrs < 24) return fmt(hrs, "h");
+ const days = Math.floor(hrs / 24);
+ if (days < 30) return fmt(days, "d");
+ const months = Math.floor(days / 30);
+ return fmt(months, "mo");
+};
+
+const formatDateTime = (dateString) => {
+ if (!dateString) return "";
+ const d = new Date(dateString);
+ if (Number.isNaN(d.getTime())) return "";
+ return d.toLocaleString(undefined, {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ });
};
export {
@@ -388,4 +404,5 @@ export {
extractFormulaExpression,
validateFormulaExpression,
getRelativeTime,
+ formatDateTime,
};
diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx
index 310b09f0..2bd50b06 100644
--- a/frontend/src/ide/run-history/Runhistory.jsx
+++ b/frontend/src/ide/run-history/Runhistory.jsx
@@ -15,15 +15,23 @@ import {
ReloadOutlined,
CalendarOutlined,
DatabaseOutlined,
+ CheckCircleFilled,
CloseCircleFilled,
+ ClockCircleOutlined,
+ SyncOutlined,
} from "@ant-design/icons";
+import { useSearchParams } from "react-router-dom";
import { useAxiosPrivate } from "../../service/axios-service";
import { orgStore } from "../../store/org-store";
import { useNotificationService } from "../../service/notification-service";
import { runHistoryTagColor } from "../../common/constants";
import { usePagination } from "../../widgets/hooks/usePagination";
-import { getTooltipText } from "../../common/helpers";
+import {
+ getTooltipText,
+ getRelativeTime,
+ formatDateTime,
+} from "../../common/helpers";
import "./RunHistory.css";
/* ─── Parse duration string to milliseconds for sorting ─── */
@@ -102,35 +110,35 @@ const Runhistory = () => {
const { selectedOrgId } = orgStore();
const { token } = theme.useToken();
const { notify } = useNotificationService();
+ const [searchParams, setSearchParams] = useSearchParams();
/* ─── API calls ─── */
- const getRunHistoryList = async (
- Id,
- page = currentPage,
- limit = pageSize
- ) => {
- setLoading(true);
- try {
- const res = await axios({
- method: "GET",
- url: `/api/v1/visitran/${
- selectedOrgId || "default_org"
- }/project/_all/jobs/run-history/${Id}`,
- params: { page, limit },
- });
- const { page_items, total_items, current_page } = res.data.data;
- setTotalCount(total_items);
- setCurrentPage(current_page);
- const { env_type, job_name, run_history, id } = page_items;
- setEnvInfo({ env_type, job_name, id });
- setJobHistoryData(run_history);
- setBackUpData(run_history);
- } catch (error) {
- notify({ error });
- } finally {
- setLoading(false);
- }
- };
+ const getRunHistoryList = useCallback(
+ async (Id, page = currentPage, limit = pageSize) => {
+ setLoading(true);
+ try {
+ const res = await axios({
+ method: "GET",
+ url: `/api/v1/visitran/${
+ selectedOrgId || "default_org"
+ }/project/_all/jobs/run-history/${Id}`,
+ params: { page, limit },
+ });
+ const { page_items, total_items, current_page } = res.data.data;
+ setTotalCount(total_items);
+ setCurrentPage(current_page);
+ const { env_type, job_name, run_history, id } = page_items;
+ setEnvInfo({ env_type, job_name, id });
+ setJobHistoryData(run_history);
+ setBackUpData(run_history);
+ } catch (error) {
+ notify({ error });
+ } finally {
+ setLoading(false);
+ }
+ },
+ [axios, selectedOrgId, currentPage, pageSize, notify]
+ );
const getJobList = async () => {
setLoading(true);
@@ -144,17 +152,26 @@ const Runhistory = () => {
const { page_items } = res.data.data;
const scheduledObj = {};
const jobIds = page_items.map((el) => {
- scheduledObj[el.user_task_id] = getTooltipText(
- el.periodic_task_details[el.task_type],
- el.task_type
- );
+ const taskDetails = el.periodic_task_details?.[el.task_type];
+ if (taskDetails) {
+ scheduledObj[el.user_task_id] = getTooltipText(
+ taskDetails,
+ el.task_type
+ );
+ }
return { label: el.task_name, value: el.user_task_id };
});
setJobSchedule(scheduledObj);
setJobListItems(jobIds);
if (jobIds.length) {
- setFilterQuery((prev) => ({ ...prev, job: jobIds[0].value }));
- getRunHistoryList(jobIds[0].value);
+ const taskFromUrl = searchParams.get("task");
+ const taskFromUrlNum = taskFromUrl ? Number(taskFromUrl) : NaN;
+ const matchedFromUrl = !Number.isNaN(taskFromUrlNum)
+ ? jobIds.find((j) => j.value === taskFromUrlNum)
+ : null;
+ const initial = matchedFromUrl?.value ?? jobIds[0].value;
+ setFilterQuery((prev) => ({ ...prev, job: initial }));
+ getRunHistoryList(initial);
}
} catch (error) {
console.error("Failed to load jobs", error);
@@ -191,7 +208,7 @@ const Runhistory = () => {
backUpData,
]);
- /* ─── auto-expand failed rows on fresh data load ─── */
+ /* ─── auto-expand failed rows on fresh data load (not on filter changes) ─── */
useEffect(() => {
const failedIds = (backUpData || [])
.filter((r) => r.status === "FAILURE" && r.error_message)
@@ -200,10 +217,22 @@ const Runhistory = () => {
}, [backUpData]);
/* ─── handlers ─── */
- const handleJobChange = useCallback((value) => {
- setFilterQuery({ status: "", job: value, trigger: "", scope: "" });
- getRunHistoryList(value);
- }, []);
+ const handleJobChange = useCallback(
+ (value) => {
+ setFilterQuery({ status: "", job: value, trigger: "", scope: "" });
+ getRunHistoryList(value);
+ setSearchParams(
+ (prev) => {
+ const next = new URLSearchParams(prev);
+ if (value) next.set("task", String(value));
+ else next.delete("task");
+ return next;
+ },
+ { replace: true }
+ );
+ },
+ [setSearchParams, getRunHistoryList]
+ );
const handleTriggerChange = useCallback((value) => {
setFilterQuery((prev) => ({ ...prev, trigger: value || "" }));
@@ -298,9 +327,25 @@ const Runhistory = () => {
return new Date(a.start_time) - new Date(b.start_time);
},
defaultSortOrder: "descend",
- render: (text) => (
-