Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion novem/cli/gql.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def _query(self, query: str, variables: Optional[Dict[str, Any]] = None) -> Dict
type
}
last_run_status
last_run_time
run_count
job_steps
current_step
Expand Down Expand Up @@ -318,7 +319,8 @@ def _transform_jobs_response(items: List[Dict[str, Any]]) -> List[Dict[str, Any]
"""
Transform GraphQL jobs response for job listing.

Includes job-specific fields: last_run_status, run_count, job_steps, current_step, schedule, triggers.
Includes job-specific fields: last_run_status, last_run_time, run_count, job_steps,
current_step, schedule, triggers.
"""
result = []
for item in items:
Expand All @@ -332,6 +334,7 @@ def _transform_jobs_response(items: List[Dict[str, Any]]) -> List[Dict[str, Any]
"shared": _transform_shared(item.get("public", False), item.get("shared", [])),
"fav": _get_markers(item.get("tags", [])),
"last_run_status": item.get("last_run_status", ""),
"last_run_time": item.get("last_run_time", ""),
"run_count": item.get("run_count", 0),
"job_steps": item.get("job_steps", 0),
"current_step": item.get("current_step"),
Expand Down Expand Up @@ -1201,6 +1204,7 @@ def list_org_group_members_gql(gql: NovemGQL, org_id: str, group_id: str, curren
shared { id name type }
tags { id name type }
author { username }
last_run_time
}
}
}
Expand Down
79 changes: 79 additions & 0 deletions novem/cli/vis.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,12 @@ def fav_fmt(markers: str, cl: cl) -> str:
"fmt": status_fmt,
"overflow": "keep",
},
{
"key": "_last_run",
"header": "Last Run",
"type": "text",
"overflow": "keep",
},
{
"key": "shared",
"header": "Shared",
Expand Down Expand Up @@ -847,6 +853,9 @@ def fav_fmt(markers: str, cl: cl) -> str:
run_count = p.get("run_count")
p["run_count"] = str(run_count) if run_count is not None else ""

# Last run - format last_run_time as relative time
p["_last_run"] = _format_time_ago(p.get("last_run_time", ""))

# Calculate max widths for right-aligned columns (must be at least header width)
max_steps = max(max((len(p["_steps"]) for p in plist), default=0), len("Steps"))
max_runs = max(max((len(p["run_count"]) for p in plist), default=0), len("Runs"))
Expand Down Expand Up @@ -908,6 +917,58 @@ def _format_relative_time(date_str: str) -> str:
return date_str


def _format_time_ago(date_str: str) -> str:
"""Format a date string as compact relative time.

Uses compact format: "1 min ago", "2 hrs ago", "1 day ago", etc.
"""
if not date_str:
return ""
try:
parsed = eut.parsedate(date_str)
if not parsed:
return date_str
dt = datetime.datetime(*parsed[:6])
now = datetime.datetime.now()
delta = now - dt

if delta.days < 0:
return "in the future"
elif delta.days == 0:
if delta.seconds < 60:
return "just now"
elif delta.seconds < 3600:
mins = delta.seconds // 60
return f"{mins} min ago"
else:
hours = delta.seconds // 3600
if hours == 1:
return "1 hour ago"
else:
return f"{hours} hrs ago"
elif delta.days == 1:
return "1 day ago"
elif delta.days < 7:
return f"{delta.days} days ago"
elif delta.days < 14:
return "1 week ago"
elif delta.days < 30:
weeks = delta.days // 7
return f"{weeks} weeks ago"
elif delta.days < 60:
return "1 month ago"
elif delta.days < 365:
months = delta.days // 30
return f"{months} months ago"
elif delta.days < 730:
return "1 year ago"
else:
years = delta.days // 365
return f"{years} years ago"
except Exception:
return date_str


def list_orgs(args: Dict[str, Any]) -> None:
"""List organizations with custom formatting."""
colors()
Expand Down Expand Up @@ -1549,7 +1610,25 @@ def fav_fmt(markers: str, _cl: Any) -> str:
},
]

# Add Last Run column for jobs only (after Type column)
if vis_type == "Job":
# Find Type column index and insert after it
type_idx = next((i for i, col in enumerate(ppo) if col.get("key") == "type"), -1)
ppo.insert(
type_idx + 1,
{
"key": "_last_run",
"header": "Last Run",
"type": "text",
"overflow": "keep",
},
)

for p in plist:
# Format last_run for jobs
if vis_type == "Job":
p["_last_run"] = _format_time_ago(p.get("last_run_time", ""))

if p.get("updated"):
parsed = eut.parsedate(p["updated"])
if parsed:
Expand Down