Skip to content

feat: redesign Jobs List page with stats dashboard, rich table, and next-run fix#73

Open
wicky-zipstack wants to merge 8 commits intomainfrom
feat/jobs-list-redesign
Open

feat: redesign Jobs List page with stats dashboard, rich table, and next-run fix#73
wicky-zipstack wants to merge 8 commits intomainfrom
feat/jobs-list-redesign

Conversation

@wicky-zipstack
Copy link
Copy Markdown
Contributor

@wicky-zipstack wicky-zipstack commented Apr 24, 2026

What

  • Redesign the Jobs List page with a stats dashboard, enriched table columns, enhanced filters, and status-aware job icons
  • Fix backend _compute_next_run_time() bug that returned past timestamps, hiding the "in Xm" countdown tag

Why

  • The existing Jobs List was a basic table with plain text columns (separate "Last Run Status" and "Last Run" columns, no visual cues for job health, no at-a-glance overview)
  • Filters were limited to search, environment, and project — no way to filter by status, schedule type, or last run state
  • _compute_next_run_time() used stale last_run_at as reference for remaining_estimate(), which returns a negative duration when the reference is old — making now + remaining a past timestamp and the countdown tag never rendering

How

Frontend — JobList.jsx (page container):

  • Added stats dashboard row with 4 cards: Active Jobs (with paused count), Success Rate 24h, Failed Runs 24h, Next Run countdown — all computed client-side from job list data
  • Added page header with "Jobs" title, subtitle, and "Create Job" button
  • Added client-side filtering for status, environment type, and schedule type (previously only search and project/env)

Frontend — JobListTable.jsx (table component):

  • Job column: Added circular status icon with colored background next to job name — green check (success), red X (failed), grey pause (paused), blue clock (scheduled), spinning sync (running) — each with descriptive tooltip
  • Environment column: Replaced plain text with styled badges — PROD (red), STG (warning), DEV (blue) with icons and monospace env name
  • Schedule column: Replaced plain text with CRON/INTERVAL tags showing cron expression and human-readable description
  • Last Run column: Consolidated old "Last Run Status" and "Last Run" into one column with status tag + formatted date + relative time + duration
  • Next Run column: Added "in Xm/Xh/Xd" countdown tag below the date
  • Actions column: Added "Run now" and "Run history" buttons alongside existing Edit/Delete

Frontend — JobListFilters.jsx:

  • Wrapped filters in a bordered Card with job count badge
  • Added Status, Last Run, and Schedule Type filter dropdowns
  • Added "Clear filters" and "Refresh" buttons

Frontend — JobDeploy.css:

  • Added .jl-job-icon styles with .success, .failed, .running, .paused color variants
  • Added @keyframes jl-pulse animation for running/scheduled state

Backend — views.py (_compute_next_run_time):

  • Changed reference from last_run_at or periodic.last_run_at to timezone.now() — ensures remaining_estimate() always returns a positive duration for the next upcoming occurrence
  • Prioritized fresh computation over stored task.next_run_time (which can also be stale)
  • Changed exception logging from debug to warning for visibility

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No breaking changes. The next_run_time field in the API response has the same type (ISO datetime string or null) — only the computation is corrected. All frontend changes are presentational — same data source, new rendering. No props or API contracts changed.

Database Migrations

  • None

Env Config

  • None

Relevant Docs

  • N/A

Related Issues or PRs

  • N/A

Dependencies Versions

  • No new dependencies added

Notes on Testing

  • Verify stats cards show correct counts matching the table data
  • Verify "in Xm" countdown tag appears in Next Run column for jobs with future scheduled runs
  • Verify countdown disappears for paused jobs and jobs with no next run
  • Verify job status icons: green check (last run succeeded), red X (failed), grey pause (disabled), blue clock (never run / scheduled), spinning blue (running)
  • Verify hovering icons shows tooltip text ("Healthy — last run succeeded", "Last run failed — needs attention", etc.)
  • Verify environment badges render PROD/STG/DEV with correct colors
  • Verify schedule badges show cron expression for CRON jobs and label for INTERVAL jobs
  • Verify all filters (search, status, environment, schedule type) correctly narrow the table
  • Verify Clear filters resets all dropdowns
  • Verify Run Now, History, Edit, Delete actions still work
  • Verify Enable/Disable toggle updates job and refreshes list

Screenshots

image ## Checklist

Stats cards:
- Active Jobs (with paused count)
- Success Rate (24H) — computed client-side
- Failed Runs (24H) with attention indicator
- Next Run (countdown + job name)

Table columns redesigned:
- Job: icon + name (link) + description
- Environment: PROD/STG/DEV styled badges with env name
- Schedule: CRON/INTERVAL tags with expression + description
- Last Run: status tag + date + relative time + duration
- Next Run: date + "in Xm" countdown badge, "Paused" when disabled
- Status: toggle switch + label
- Actions: Run now, History, Edit, Delete icons

Filters redesigned:
- Inside bordered Card (matches Run History)
- Search, Status, Last run, Environment, Schedule dropdowns
- Job count + Clear filters link + Refresh

Layout: matches Run History page width pattern
- CheckCircleFilled green bg → "Healthy — last run succeeded"
- CloseCircleFilled red bg → "Last run failed — needs attention"
- PauseCircleOutlined grey bg → "Paused — will not run"
- ClockCircleOutlined blue bg + pulse → "Scheduled — has not run yet"
- SyncOutlined spinning blue bg + pulse → "Running"
- Circular icon with 12% opacity background matching status color
- Tooltip on hover with descriptive status message
…n_at

remaining_estimate(last_run_at) returns a negative duration when
last_run_at is old, producing a past timestamp and hiding the
countdown tag in the Jobs List UI.
@wicky-zipstack wicky-zipstack requested review from a team as code owners April 24, 2026 15:33
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR redesigns the Jobs List page with a stats dashboard (active jobs, 24h success/failure metrics, next-run countdown), enriched table columns (status icons, environment badges, schedule badges, consolidated Last Run + duration), additional client-side filters (status, last run window, schedule type), and fixes the backend _compute_next_run_time() bug that produced past timestamps by switching the remaining_estimate reference to timezone.now().

  • Project filter silently dropped (flagged in previous review, still unresolved): JobListFilters.jsx no longer renders a Project dropdown, but JobList.jsx still fetches projects, maintains filters.proj in state, and applies el.project?.name === proj in the filter useEffect — users have no UI control to set it, locking it permanently at \"all\".
  • IntervalSchedule next-run overstatement (flagged in previous review, reply claims fixed in 246b19ca2 but the mechanism in this diff still passes now as last_run_at, which always returns the full interval period rather than the time remaining until the next tick).

Confidence Score: 4/5

Safe to merge with awareness of the still-open project-filter P1 from the previous review cycle.

All three P1s raised in the previous review were replied to as fixed; two are confirmed fixed in this diff (lastRun filter applied, runSearch race removed, statusClass corrected, onRowClick in deps). The project filter removal remains an unresolved P1. The IntervalSchedule overstatement reply claims a fix in 246b19c, but the current diff mechanism still exhibits the original behaviour for interval jobs. No new P0s or P1s were found in the remaining changes.

frontend/src/ide/scheduler/JobListFilters.jsx — project filter UI removed while the filter logic and prop plumbing remain; frontend/src/ide/scheduler/JobList.jsx — stats and filters operate on the current server page (backup) rather than the full job fleet.

Important Files Changed

Filename Overview
backend/backend/core/scheduler/views.py Fixes _compute_next_run_time() to always use timezone.now() as the reference, preventing stale past timestamps; also inverts priority to prefer fresh computation over cached next_run_time
frontend/src/ide/scheduler/JobDeploy.css Adds .jl-job-icon status variant classes (success/failed/running/paused) and @Keyframes jl-pulse animation for the running state indicator
frontend/src/ide/scheduler/JobList.jsx Adds stats dashboard (active jobs, 24h success rate, 24h failed runs, next-run countdown), moves Create Job to page header, resolves lastRun filter and runSearch race condition from previous review; stats are still computed from backup which is page-scoped
frontend/src/ide/scheduler/JobListFilters.jsx Redesigned to a Card-wrapped filter bar with Status, Last Run, Environment, and Schedule dropdowns; project filter UI removed while the backend filter logic in JobList.jsx still references filters.proj — project filtering is irreversibly broken for end users
frontend/src/ide/scheduler/JobListTable.jsx Enriched table with circular status icons, EnvironmentBadge, ScheduleBadge, consolidated Last Run column with duration, Next Run countdown tag, and Run now / History action buttons; previous icon-class and useMemo dep issues from prior review resolved

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[JobList mounts] --> B[fetchJobs - server page]
    B --> C[backup = page_items]
    C --> D[stats useMemo\nactive / paused / 24h success\n/ 24h failed / nextRun]
    C --> E[filter useEffect\nproj · env · status · lastRun · schedule · search]
    E --> F[jobList - displayed in table]
    F --> G[JobListTable]
    G --> H[Job column\nstatus icon + name]
    G --> I[Environment\nEnvironmentBadge]
    G --> J[Schedule\nScheduleBadge]
    G --> K[Last Run\nstatus tag + date + duration]
    G --> L[Next Run\ndate + countdown tag]
    G --> M[Actions\nRun now · History · Edit · Delete]
    D --> N[StatCard × 4\nActive · Success rate · Failed · Next run]
    B2[_compute_next_run_time\nschedule.remaining_estimate now] --> O[next_run_time in API response]
    O --> C
Loading

Reviews (6): Last reviewed commit: "fix: append (UTC) to cron schedule descr..." | Re-trigger Greptile

Comment thread frontend/src/ide/scheduler/JobList.jsx
Comment thread frontend/src/ide/scheduler/JobList.jsx
Comment thread frontend/src/ide/scheduler/JobListTable.jsx Outdated
…duled icon

- Remove runSearch debounce that raced with useEffect and dropped active filters
- Apply lastRun filter (24h/7d/30d) using task_completion_time cutoff
- Compute success rate and failed count from last 24h only (not all-time)
- Use "paused" CSS class for scheduled/never-run jobs instead of "running"
- Add onRowClick to useMemo dependency array
- Remove unused formatDurationMs from JobList
- Prettier formatting for all 4 files
- eslint-disable for eqeqeq and react/prop-types
- Remove unused imports (Badge, PlusOutlined)
- Split let declarations (one-var rule)
- Suppress no-unused-vars for destructuring pattern
Copy link
Copy Markdown
Contributor

@abhizipstack abhizipstack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

@tahierhussain tahierhussain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice redesign overall — the stats dashboard, status icons, and rich Schedule/Last Run columns are a clear UX upgrade, and the _compute_next_run_time fix is correct.

Requesting changes on four items before merge:

  1. JobListTable.jsxScheduleBadge data shape — the access path changed from periodic_task_details[task_type] to details.cron.cron_expression / details.interval. Please confirm this matches the actual API response; mismatches render silently.
  2. JobListTable.jsx — duration math — confirm task_run_time is a start timestamp, not a duration. If it's a duration, the calculation produces nonsense values that the ms > 0 guard hides.
  3. JobList.jsx — search debounce removed — either re-add it or add a comment justifying the synchronous-per-keystroke filter.
  4. JobList.jsx / JobListTable.jsx — file-wide eslint-disable — please declare PropTypes for the inline helpers (or scope the disable) instead of silencing rules project-wide.

Inline comments have details and suggested patches.

Comment thread frontend/src/ide/scheduler/JobListTable.jsx
Comment thread frontend/src/ide/scheduler/JobListTable.jsx
Comment thread frontend/src/ide/scheduler/JobList.jsx
@@ -1,6 +1,23 @@
/* eslint-disable eqeqeq, react/prop-types */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't disable react/prop-types (and eqeqeq) for the entire file.

This silences two ESLint rules across the whole module — including JobList itself and anything added to this file later — just to avoid declaring prop types for the inline StatCard helper at line 44. Disabling eqeqeq is especially concerning since there's no == in the diff; it removes the safety net for future edits too.

Please pick one of:

1. Declare PropTypes inline (preferred — ~5 lines):

StatCard.propTypes = {
  label: PropTypes.string.isRequired,
  icon: PropTypes.node,
  value: PropTypes.node.isRequired,
  valueColor: PropTypes.string,
  subtext: PropTypes.node,
};

Then drop this file-wide disable.

2. Move StatCard to its own file with its own PropTypes.

3. At minimum, scope the disable to just StatCard:

// eslint-disable-next-line react/prop-types
const StatCard = ({ label, icon, value, valueColor, subtext }) => ( ... );

Same comment applies to JobListTable.jsx:1 which has /* eslint-disable react/prop-types */ for EnvironmentBadge / ScheduleBadge.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The != null pattern is intentional — it checks both null and undefined (API returns undefined for missing fields, !== null would break). react/prop-types is disabled because StatCard is an inline component not worth a full PropTypes declaration. Same pattern used in the Run History file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the PR since this isn't a blocker, but flagging two specifics on this thread for follow-up:

  1. The eqeqeq reply is about a different rule. Your reply explains why != null is intentional, but there's no != null (or any ==) in JobList.jsx — I just re-read the file. The eqeqeq disable was added in 5a8323a defensively without a corresponding usage to justify it. Since nothing in the file uses ==, the rule should just be removed from the disable line.

  2. The react/prop-types reply skipped the lightweight option. My ask wasn't to declare full PropTypes for StatCard — option 3 in the comment was a single-line scoped disable directly above StatCard:

    // eslint-disable-next-line react/prop-types
    const StatCard = ({ ... }) => ( ... );

    That keeps the rule active for JobList itself and anything added to the file later, at zero extra cost. "Same pattern as Run History" is fair as precedent, but if Run History also has a file-wide disable, both files would benefit from the scoped form.

Not blocking — feel free to handle as a follow-up cleanup if it's worth doing.

Copy link
Copy Markdown
Contributor

@tahierhussain tahierhussain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the three correctness/UX items from my earlier review are addressed (API shape verified, task_run_time semantics confirmed, search debounce removal explained as intentional fix for the Greptile race). The remaining eslint-disable thread is code hygiene only and not blocking; left a follow-up note there for whenever you're cleaning up.

Nice redesign overall.

onDelete was removing the job from jobList but not from backup.
When filters were cleared, the useEffect re-derived jobList from
the stale backup, re-populating the deleted job.
Comment thread backend/backend/core/scheduler/views.py
totalCount was stale after deleting jobs — pagination footer still
showed the old count. Now decremented on each successful delete.
Stats cards already re-compute from backup (fixed earlier).
Comment on lines +361 to +409
onClick={() => navigate("/project/setting/subscriptions")}
>
Upgrade
</Button>
}
/>
)}

{/* Stats Cards */}
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col span={6}>
<StatCard
label="Active jobs"
icon={
<ThunderboltOutlined style={{ color: token.colorPrimary }} />
}
value={stats.activeJobs}
subtext={
stats.pausedJobs > 0 && (
<Text type="secondary">{stats.pausedJobs} paused</Text>
)
}
/>
)}
</Col>
<Col span={6}>
<StatCard
label="Success rate (24h)"
icon={<CheckCircleFilled style={{ color: token.colorSuccess }} />}
value={
stats.successRate != null ? `${stats.successRate}%` : "— %"
}
valueColor={
stats.successRate === 100
? token.colorSuccess
: stats.successRate > 0
? token.colorWarning
: undefined
}
/>
</Col>
<Col span={6}>
<StatCard
label="Failed runs (24h)"
icon={<CloseCircleFilled style={{ color: token.colorError }} />}
value={stats.failedJobs}
valueColor={stats.failedJobs > 0 ? token.colorError : undefined}
subtext={
stats.failedJobs > 0 && (
<Text style={{ color: token.colorError, fontSize: 11 }}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stats cards only reflect the current page, not all jobs

backup is assigned from page_items in fetchJobs (a single server page, typically 10–25 items). The stats useMemo reads from backup, so "Active Jobs", "Failed Runs (24h)", and "Success Rate (24h)" only count jobs visible on the current page. A user with 100 jobs across four pages could see "Active Jobs: 7" when there are 70 active jobs in total. Because the cards carry no "this page only" qualifier, users will treat the numbers as fleet-wide aggregates.

Either fetch an aggregated stats endpoint, or add a "(this page)" qualifier to each card label until the backend exposes totals.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/ide/scheduler/JobList.jsx
Line: 361-409

Comment:
**Stats cards only reflect the current page, not all jobs**

`backup` is assigned from `page_items` in `fetchJobs` (a single server page, typically 10–25 items). The `stats` `useMemo` reads from `backup`, so "Active Jobs", "Failed Runs (24h)", and "Success Rate (24h)" only count jobs visible on the current page. A user with 100 jobs across four pages could see "Active Jobs: 7" when there are 70 active jobs in total. Because the cards carry no "this page only" qualifier, users will treat the numbers as fleet-wide aggregates.

Either fetch an aggregated stats endpoint, or add a "(this page)" qualifier to each card label until the backend exposes totals.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Cron schedules are stored and executed in UTC. The description text
now shows "(UTC)" so users understand the schedule timezone context
(e.g., "At 30 minutes past the hour (UTC)").
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants