Skip to content

feat: Deploy & Run History follow-ups — polling, metrics, deep-links, DB columns, server-side filters#64

Open
abhizipstack wants to merge 16 commits intomainfrom
feat/deploy-runhistory-followups
Open

feat: Deploy & Run History follow-ups — polling, metrics, deep-links, DB columns, server-side filters#64
abhizipstack wants to merge 16 commits intomainfrom
feat/deploy-runhistory-followups

Conversation

@abhizipstack
Copy link
Copy Markdown
Contributor

What

Implements 6 of 7 items from the deferred follow-ups ticket, plus bug fixes found during testing.

Deep-link toast → Run History (#5)

  • Quick Deploy success toast now includes a clickable "View in Run History →" link (with renderMarkdown: false so JSX renders correctly).
  • Run History page auto-expands the most recent run when arriving via deep-link.

Pre-fill create-job from 0-candidates (#6)

  • "Go to Scheduler" CTA navigates to /project/job/list?create=1&project=<pid>&model=<name>.
  • Jobs List reads params: auto-opens the create drawer with the model pre-enabled in Model Configuration.

First-class trigger + scope DB columns (#4)

  • Added trigger (scheduled/manual) and scope (job/model) as real CharField columns on TaskRunHistory with DB indexes.
  • Migration 0002_taskrunhistory_trigger_scope.
  • trigger_scheduled_run writes both columns and kwargs (backward compat).
  • Frontend getRunTriggerScope prefers top-level fields, falls back to kwargs for pre-migration rows.

Server-side Run History filtering (#7)

  • task_run_history endpoint accepts ?trigger=, ?scope=, ?status= query params.
  • Frontend filter changes now trigger a server-side refetch instead of client-side filtering, so pagination is accurate across all pages.

Live deploy progress polling (#3)

  • After dispatch, Quick Deploy button flips to "Deploying…" with spinner.
  • Polls latest run status every 5s via getLatestRunStatus.
  • On terminal state: completion toast (success/failure) with Run History link, explorer refresh, recent-runs cache clear.
  • Auto-cleans on unmount.

Runtime metrics in Run History (#1)

  • After DAG execution (success or failure), trigger_scheduled_run serializes BASE_RESULT into TaskRunHistory.result as JSON.
  • Per-model name, status, end_status; aggregate total/passed/failed counts.
  • Source classes (e.g. SourceMdoela) filtered out — only user-created models appear.
  • Frontend insights panel renders a metrics bar when result.total > 0.

Deferred to separate tickets:

  • User-facing activity logs → OR-1462
  • Row counts per model → OR-1461

Why

These follow-ups close gaps identified during the Quick Deploy and Run History work: no way to navigate from a toast to the specific run, no way to create a job from the 0-candidates flow, filters were client-side only (broke pagination), no live feedback during deploy, and no execution metrics on completed runs.

How

  • Backend: migration for trigger/scope columns, query-param filtering in task_run_history, BASE_RESULT serialization in trigger_scheduled_run + _mark_failure.
  • Frontend: renderMarkdown: false for JSX toasts, useSearchParams for deep-link + pre-fill, polling via setInterval + getLatestRunStatus, metrics bar in insights panel.

Can this PR break any existing features?

Low risk:

  • New DB columns have defaults (scheduled / job) so existing rows are valid post-migration. Serializer uses fields = "__all__" so new columns auto-expose.
  • Server-side filtering is additive — no params = no filter = same behavior as before.
  • Polling is self-contained in component state; auto-cleans on unmount.
  • result field was already on the model (never written); now populated. Frontend guards with result?.total > 0.
  • Source class filtering uses startswith("Source") convention from the no-code model generator.

Database Migrations

  • backend/backend/core/scheduler/migrations/0002_taskrunhistory_trigger_scope.py — adds trigger, scope columns + indexes.

Env Config

None.

Relevant Docs

None.

Related Issues or PRs

Dependencies Versions

No changes.

Notes on Testing

Tested locally (gunicorn + Celery worker + React dev server):

  1. Quick Deploy → success toast shows clickable "View in Run History →" link → navigates to Run History with job preselected + first run auto-expanded.
  2. Quick Deploy on model with no job → "Go to Scheduler" → Jobs List opens with create drawer, model pre-checked.
  3. Run History filters (Trigger/Scope/Status) now refetch from server — verified pagination accuracy.
  4. Quick Deploy → button shows "Deploying…" spinner → polls → completion toast appears on SUCCESS/FAILURE.
  5. Run History expanded row shows metrics bar: "1 model attempted · 1 passed · 0 failed · Mdoela (OK)". Source classes filtered out.
  6. Migration applied cleanly; old runs render with kwargs fallback.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

abhizipstack and others added 12 commits April 16, 2026 17:53
The Quick Deploy success toast now includes a clickable "View in Run
History →" link that navigates to /project/job/history?task=<id>,
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) <noreply@anthropic.com>
When no job covers the current model, clicking "Go to Scheduler" now
navigates to /project/job/list?create=1&project=<pid>&model=<name>.
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
The notify service defaults to renderMarkdown: true, which wraps
description in ReactMarkdown. When description is JSX (our <a> 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
BASE_RESULT.node_name stores str(cls) which renders as
<class 'project.models.mdoela.Mdoela'>. 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@abhizipstack abhizipstack requested review from a team as code owners April 16, 2026 13:40
Comment thread frontend/src/ide/editor/no-code-model/no-code-model.jsx Fixed
Comment thread frontend/src/ide/editor/no-code-model/no-code-model.jsx Fixed
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 16, 2026

Greptile Summary

This PR completes six follow-up items from the Deploy & Run History track: live deploy polling, deep-link navigation to Run History, pre-fill create-job from the 0-candidates CTA, first-class trigger/scope DB columns with migration, server-side Run History filtering, and per-model execution metrics. Prior review concerns (stale BASE_RESULT, _mark_failure reading a cleared global, pagination reset, missing project URL param) have all been addressed.

  • P1filterQueries.job in the server-side filter effect's dep array causes a concurrent double-fetch race on every job change, because handleJobChange and getJobList also call getRunHistoryList directly. If the stale-page response resolves last it overwrites the correct page-1 data.
  • P2searchParams.has(\"task\") never clears, so the deep-link auto-expand fires on every data refresh rather than only on first arrival.

Confidence Score: 4/5

Safe to merge after resolving the double-fetch race in Runhistory.jsx; all prior P1 issues are fixed.

One unresolved P1 (double-fetch race on job change that can leave stale page-N data visible) prevents a 5. Backend changes, migration, polling, and pre-fill flows are solid.

frontend/src/ide/run-history/Runhistory.jsx — filter effect dep array + direct getRunHistoryList calls create concurrent requests.

Important Files Changed

Filename Overview
frontend/src/ide/run-history/Runhistory.jsx Adds server-side filtering, deep-link auto-expand, and metrics bar; introduces a double-fetch race condition on job change.
backend/backend/core/scheduler/celery_tasks.py Adds BASE_RESULT metrics capture with snapshotting before clear; introduces duplicate _clean_name and _clean inner functions.
backend/backend/core/scheduler/migrations/0002_taskrunhistory_trigger_scope.py Adds trigger/scope CharField columns with indexes; default=scheduled/job means old manual runs will be labelled scheduled in DB.
backend/backend/core/scheduler/models.py Adds trigger and scope CharField fields + indexes matching the migration; clean and straightforward.
backend/backend/core/scheduler/views.py Adds server-side trigger/scope/status filtering to task_run_history endpoint; additive and non-breaking.
frontend/src/ide/editor/no-code-model/no-code-model.jsx Adds polling loop for live deploy status, JSX toast with Run History deep-link, and goToScheduler pre-fill params; cleanup on unmount is correct.
frontend/src/ide/scheduler/JobDeploy.jsx Adds prefillModel and prefillProject props with useEffect handlers; straightforward and well-guarded.
frontend/src/ide/scheduler/JobList.jsx Reads create/model/project URL params, opens JobDeploy drawer with pre-fill, clears params via replace.
frontend/src/ide/scheduler/service.js Adds getLatestRunStatus fetching page 1 limit 1; correctly extracts the first run from the response.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant NCM as NoCodeModel
    participant SVC as service.js
    participant API as Django API
    participant RH as RunHistory

    U->>NCM: Click Quick Deploy
    NCM->>API: POST runTask / runTaskForModel
    API-->>NCM: 200 OK dispatched
    NCM->>U: Toast Deploy Triggered + View in Run History link
    NCM->>NCM: startDeployPolling(taskId)

    loop Every 5s while non-terminal
        NCM->>SVC: getLatestRunStatus(projectId, taskId)
        SVC->>API: GET /run-history/{taskId}?page=1&limit=1
        API-->>SVC: run status object
        SVC-->>NCM: run object
    end

    NCM->>U: Toast Deploy Completed/Failed + View in Run History link
    NCM->>NCM: clearInterval + refresh caches

    U->>RH: Navigate to /project/job/history?task=taskId
    RH->>API: GET /run-history/{taskId}?page=1&limit=10
    API-->>RH: runs with trigger/scope columns
    RH->>RH: Auto-expand most recent run
    U->>RH: Change filter
    RH->>API: GET /run-history/{taskId}?trigger=X&scope=Y
    API-->>RH: Server-side filtered runs
Loading

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: frontend/src/ide/run-history/Runhistory.jsx
Line: 195-209

Comment:
**Double-fetch race condition on job change**

Adding `filterQueries.job` to this effect's dep array means the effect fires whenever the job changes — but `handleJobChange` (line 229) and `getJobList` (line 181) already call `getRunHistoryList` directly after calling `setFilterQuery`. This produces two concurrent requests on every job switch. The direct call uses `currentPage` (which may be > 1 from the previous job), while the effect always passes `page=1`. Whichever response resolves last wins; if the stale-page response lands second it overwrites the correct page-1 data.

The fix is to remove the direct `getRunHistoryList(value)` calls from `handleJobChange` and `getJobList` and let this effect be the sole fetch trigger.

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

---

This is a comment left during a code review.
Path: frontend/src/ide/run-history/Runhistory.jsx
Line: 212-224

Comment:
**Deep-link auto-expand fires on every data refresh, not just initial arrival**

`searchParams.has("task")` stays `true` for the lifetime of the page. Because this effect depends on `backUpData`, it re-runs after every API response — page changes, filter changes, refreshes — and auto-expands `backUpData[0].id` each time. A user who navigates to page 2 will find the first item of that page unexpectedly expanded.

A `useRef` one-shot flag would limit the behaviour to arrival only:

```js
const deepLinkExpandedRef = useRef(false);
useEffect(() => {
  const ids = [];
  const fromDeepLink = !deepLinkExpandedRef.current && searchParams.has("task");
  if (fromDeepLink && backUpData.length > 0) {
    ids.push(backUpData[0].id);
    deepLinkExpandedRef.current = true;
  }
  (backUpData || [])
    .filter((r) => r.status === "FAILURE" && r.error_message)
    .forEach((r) => { if (!ids.includes(r.id)) ids.push(r.id); });
  setExpandedRowKeys(ids);
}, [backUpData, searchParams]);
```

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

---

This is a comment left during a code review.
Path: backend/backend/core/scheduler/celery_tasks.py
Line: 348-353

Comment:
**Duplicate name-cleaning logic defined twice**

`_clean_name` (success path) and `_clean` (`_mark_failure`) are byte-for-byte identical. A future edit to one will silently diverge from the other. Extract to a single module-level helper:

```python
def _clean_node_name(raw: str) -> str:
    if "'" in raw:
        return raw.split("'")[1].split(".")[-1]
    return raw
```

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

Reviews (5): Last reviewed commit: "fix: use filterQueries.job instead of en..." | Re-trigger Greptile

Comment thread backend/backend/core/scheduler/celery_tasks.py
Comment thread frontend/src/ide/scheduler/JobList.jsx
…e bugs

- 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) <noreply@anthropic.com>
@abhizipstack
Copy link
Copy Markdown
Contributor Author

All 6 issues addressed in 3c44484:

  1. Filters dropped on pagination (P1)handlePagination now passes active filterQueries to getRunHistoryList.
  2. BASE_RESULT stale global (P1) — Added snapshot-then-clear pattern + _clear_base_result() helper called in all exit paths (success, timeout, exception).
  3. handleRefresh stale closure (P2) — Updated deps to include filterQueries, currentPage, pageSize, getRunHistoryList.
  4. project URL param ignored (P2)JobList now reads project from URL params and forwards prefillProject to JobDeploy, which pre-selects it.
  5. list_recent_runs_for_model kwargs (P2) — Now prefers run.trigger/run.scope DB columns, falling back to kwargs for pre-migration rows.
  6. CodeQL XSS (DOM text as HTML) — Applied encodeURIComponent() to taskId in toast deep-link URLs.

Comment thread backend/backend/core/scheduler/celery_tasks.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread frontend/src/ide/run-history/Runhistory.jsx
@wicky-zipstack
Copy link
Copy Markdown
Contributor

wicky-zipstack commented Apr 16, 2026

A few items to consider:

1. Same repr(cls) parsing anti-pattern as #54 / #59celery_tasks.py:47

def _clean_name(raw):
    if "'" in raw:
        return raw.split("'")[1].split(".")[-1]

PR #59 already fixed this with cls.__name__ elsewhere. Better to write a clean name into BASE_RESULT.node_name upstream and parse zero times.

2. _clean_name duplicated — defined in both trigger_scheduled_run and _mark_failure. Extract to module level.

3. Polling has no max duration / backoffno-code-model.jsx:370. Hardcoded 5s interval = 360 backend hits per 30-min deploy. Add a max-poll-count or exponential backoff. Also no upper-bound timeout — if backend never returns terminal status, polling runs until unmount.

4. RETRY status not handled in terminal listno-code-model.jsx:329 — only ["SUCCESS", "FAILURE", "REVOKED"]. Button stays "Deploying…" during retries — fine if intentional, worth confirming.

5. BASE_RESULT is a module-level global — works for single-threaded Celery worker. If you ever run prefork pool with concurrency > 1 in same worker, the global will be shared/contaminated across concurrent jobs. Worth verifying worker config.

6. startswith("Source") filter is brittlecelery_tasks.py:54. A user-named SourceData model would be hidden. Use a flag on the node instead. (Tech debt — fine for now.)

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) <noreply@anthropic.com>
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.

@abhizipstack Please add screenshots for the UI changes addressed in this PR.

Comment thread frontend/src/ide/run-history/Runhistory.jsx
envInfo.id updates only after getRunHistoryList completes, creating
a race window where pagination could fetch data for the previously
selected job if the user switches jobs and changes page before the
new data arrives. filterQueries.job updates immediately on job
selection, so pagination always targets the correct job.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment on lines 195 to 209
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,
]);
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 Double-fetch race condition on job change

Adding filterQueries.job to this effect's dep array means the effect fires whenever the job changes — but handleJobChange (line 229) and getJobList (line 181) already call getRunHistoryList directly after calling setFilterQuery. This produces two concurrent requests on every job switch. The direct call uses currentPage (which may be > 1 from the previous job), while the effect always passes page=1. Whichever response resolves last wins; if the stale-page response lands second it overwrites the correct page-1 data.

The fix is to remove the direct getRunHistoryList(value) calls from handleJobChange and getJobList and let this effect be the sole fetch trigger.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/ide/run-history/Runhistory.jsx
Line: 195-209

Comment:
**Double-fetch race condition on job change**

Adding `filterQueries.job` to this effect's dep array means the effect fires whenever the job changes — but `handleJobChange` (line 229) and `getJobList` (line 181) already call `getRunHistoryList` directly after calling `setFilterQuery`. This produces two concurrent requests on every job switch. The direct call uses `currentPage` (which may be > 1 from the previous job), while the effect always passes `page=1`. Whichever response resolves last wins; if the stale-page response lands second it overwrites the correct page-1 data.

The fix is to remove the direct `getRunHistoryList(value)` calls from `handleJobChange` and `getJobList` and let this effect be the sole fetch trigger.

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

Fix in Claude Code

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.

4 participants