diff --git a/.dev.vars.example b/.dev.vars.example
index da963bf..6806e37 100644
--- a/.dev.vars.example
+++ b/.dev.vars.example
@@ -16,8 +16,7 @@ GITHUB_CLIENT_SECRET="REPLACE_WITH_YOUR_CLIENT_SECRET"
APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nREPLACE_WITH_YOUR_GITHUB_APP_PRIVATE_KEY_CONTENT\n-----END RSA PRIVATE KEY-----"
# --- Cloudflare API ---
-# Required permissions: Queues Edit for DLQ actions, Workers AI Read for
-# Cloudflare model catalog discovery.
+# Required permission: Workers AI Read for Cloudflare model catalog discovery.
CF_ACCOUNT_ID="REPLACE_WITH_YOUR_CLOUDFLARE_ACCOUNT_ID"
CF_API_TOKEN="REPLACE_WITH_CLOUDFLARE_API_TOKEN"
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..05a7337
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ed0bd40..56df09a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,6 +14,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+permissions:
+ contents: read
+
jobs:
verify:
name: Verify Stability
@@ -46,10 +49,10 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: 'npm'
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..313d1dd
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,33 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ schedule:
+ - cron: '0 0 * * 1'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ security-events: write
+
+ strategy:
+ matrix:
+ language: ['javascript-typescript']
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3.36.3
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3.36.3
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..15205df
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,28 @@
+# Changelog
+
+## v0.9.2
+
+### What's New
+
+- **UI Redesign:** Full visual overhaul of the dashboard, landing page, and auth flows with an updated color system, improved dark mode, and a cleaner layout overall.
+- **Code Splitting:** All pages now use `React.lazy` for async loading, reducing initial bundle size and improving load times.
+- **Custom LLM Providers:** Manage custom OpenAI-compatible API providers directly from the dashboard. Rate limits are optional. No more hardcoded keys in wrangler config.
+- **Cloudflare Setup Script:** A new Node.js script automates the full Cloudflare deployment setup, making self-hosting significantly easier.
+- **Increased Review Capacity:** Max files processed per review raised from 15 to 100.
+
+### Improvements
+
+- Review jobs are now resumable and lease-aware, stalled reviews can recover automatically.
+- Added retry logic for transient model provider failures.
+- Optimized job polling with ETag caching and adaptive delays.
+- Improved settings UI, error reporting, and API robustness across the board.
+- Added GitHub Actions CI with a disposable test environment for the full test suite.
+
+### Bug Fixes
+
+- Fixed `APP_PRIVATE_KEY` parsing for single-line strings with literal `\n` sequences.
+- Fixed database migration failures on existing deployments.
+- Fixed duplicate `/api/auth/updates-email` calls on page load.
+- Fixed file review status bar rendering on the dashboard.
+
+**Full Changelog**: https://github.com/devarshishimpi/codra/commits/v0.9.2
diff --git a/README.md b/README.md
index 13d2b6b..56fa3de 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ Codra listens to GitHub pull request events, runs AI-powered review jobs, posts
- **Review with repository context**: Codra checks pull request diffs for correctness, security, performance, maintainability, and repo-specific patterns.
- **Configure each repository**: Tune triggers, skipped paths, draft handling, mention reviews, labels, custom rules, and review budgets from the dashboard.
- **Route models deliberately**: Use global defaults, per-repo model chains, fallbacks, and size-based overrides for larger pull requests.
-- **Operate the system**: Inspect job history, PR findings, webhook deliveries, queue failures, DLQ replay, model usage, and dashboard stats.
+- **Operate the system**: Inspect job history, PR findings, webhook deliveries, model usage, and dashboard stats.
## Features
@@ -46,7 +46,6 @@ Codra listens to GitHub pull request events, runs AI-powered review jobs, posts
- Mention-triggered reviews for on-demand analysis
- Inline GitHub review comments plus summary reviews and check run updates
- Queue-backed processing through Cloudflare Queues
-- Dead letter queue inspection, replay, and purge workflows
- GitHub OAuth dashboard authentication
- External PostgreSQL storage through Cloudflare Hyperdrive
- Dashboard-managed LLM providers for OpenAI, OpenRouter, Anthropic, Google, and Cloudflare models
@@ -59,14 +58,14 @@ Codra listens to GitHub pull request events, runs AI-powered review jobs, posts
3. A review job is stored in PostgreSQL and queued on Cloudflare Queues.
4. The Worker consumes the job, fetches the PR diff, runs model review passes, and formats findings.
5. Codra posts inline comments and a summary review back to GitHub.
-6. The dashboard keeps the job history, findings, logs, stats, and replay tools available for operators.
+6. The dashboard keeps the job history, findings, logs, and stats available for operators.
## Stack
- **Worker**: Cloudflare Workers, Hono, Wrangler
- **Dashboard**: React, Vite, Tailwind CSS, Radix UI, Recharts
- **Data**: PostgreSQL, Cloudflare Hyperdrive, Cloudflare KV
-- **Queues**: Cloudflare Queues with DLQ workflows
+- **Queues**: Cloudflare Queues and Workflows
- **Models**: OpenAI, OpenRouter, Anthropic, Google, and Cloudflare providers
- **GitHub**: GitHub App webhooks, checks, reviews, and OAuth
- **Quality**: TypeScript, Zod, Vitest, Playwright browser tests
diff --git a/db/migrations/004_review_performance_settings.sql b/db/migrations/004_review_performance_settings.sql
new file mode 100644
index 0000000..794c252
--- /dev/null
+++ b/db/migrations/004_review_performance_settings.sql
@@ -0,0 +1,4 @@
+INSERT INTO global_settings (key, value) VALUES
+ ('review_concurrency_level', 'medium'),
+ ('review_max_comments', '10')
+ON CONFLICT (key) DO NOTHING;
diff --git a/db/migrations/005_drop_model_rate_limits.sql b/db/migrations/005_drop_model_rate_limits.sql
new file mode 100644
index 0000000..a0daa35
--- /dev/null
+++ b/db/migrations/005_drop_model_rate_limits.sql
@@ -0,0 +1,3 @@
+ALTER TABLE model_configs DROP COLUMN IF EXISTS rpm;
+ALTER TABLE model_configs DROP COLUMN IF EXISTS tpm;
+ALTER TABLE model_configs DROP COLUMN IF EXISTS rpd;
diff --git a/db/migrations/006_job_continuation_count.sql b/db/migrations/006_job_continuation_count.sql
new file mode 100644
index 0000000..2e2cc8b
--- /dev/null
+++ b/db/migrations/006_job_continuation_count.sql
@@ -0,0 +1,5 @@
+-- Tracks how many times a job has rescheduled the *same* phase without completing any
+-- file review (a "no-progress continuation"). Reset to 0 whenever a chunk makes progress.
+-- A hard ceiling on this counter (see MAX_JOB_CONTINUATIONS in review.ts) stops a job that
+-- can never make headway from churning indefinitely on transient/budget deferrals.
+ALTER TABLE jobs ADD COLUMN IF NOT EXISTS continuation_count INT NOT NULL DEFAULT 0;
diff --git a/db/migrations/007_file_review_async_batch.sql b/db/migrations/007_file_review_async_batch.sql
new file mode 100644
index 0000000..456cff2
--- /dev/null
+++ b/db/migrations/007_file_review_async_batch.sql
@@ -0,0 +1,7 @@
+-- Supports the Workers AI asynchronous Batch API path (see submitCloudflareBatch/pollCloudflareBatch).
+-- A file review submitted to the async queue is persisted with file_status = 'pending' plus the
+-- queue request_id, so a later review-phase invocation can poll for the result across invocations
+-- (decoupling long/reasoning-model inference from any single invocation's timeout & subrequest cap).
+-- Both columns are cleared once the batch completes and the review is persisted as 'done'/'failed'.
+ALTER TABLE file_reviews ADD COLUMN IF NOT EXISTS async_request_id TEXT;
+ALTER TABLE file_reviews ADD COLUMN IF NOT EXISTS async_model TEXT;
diff --git a/db/migrations/008_job_cancelled_status.sql b/db/migrations/008_job_cancelled_status.sql
new file mode 100644
index 0000000..70555b4
--- /dev/null
+++ b/db/migrations/008_job_cancelled_status.sql
@@ -0,0 +1,6 @@
+-- Adds a terminal 'cancelled' status for jobs that a user explicitly stops (see the /stop
+-- endpoint). Distinct from 'failed' (nothing went wrong) and 'superseded' (replaced by a newer
+-- job). The status column is TEXT + CHECK (not the native enum), so we just widen the CHECK.
+ALTER TABLE jobs DROP CONSTRAINT IF EXISTS jobs_status_check;
+ALTER TABLE jobs ADD CONSTRAINT jobs_status_check
+ CHECK (status IN ('queued', 'running', 'done', 'failed', 'superseded', 'cancelled'));
diff --git a/package-lock.json b/package-lock.json
index 95516b1..cf5aeb8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"clsx": "^2.1.1",
"hono": "^4.12.12",
"jsonrepair": "^3.13.3",
+ "lenis": "^1.3.25",
"lucide-react": "^1.8.0",
"motion": "^12.42.2",
"picomatch": "^4.0.4",
@@ -4772,6 +4773,37 @@
"node": ">=6"
}
},
+ "node_modules/lenis": {
+ "version": "1.3.25",
+ "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.25.tgz",
+ "integrity": "sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==",
+ "license": "MIT",
+ "workspaces": [
+ "packages/*",
+ "playground",
+ "playground/*"
+ ],
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/darkroomengineering"
+ },
+ "peerDependencies": {
+ "@nuxt/kit": ">=3.0.0",
+ "react": ">=17.0.0",
+ "vue": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@nuxt/kit": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "vue": {
+ "optional": true
+ }
+ }
+ },
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
diff --git a/package.json b/package.json
index 682e74a..2fce114 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
"clsx": "^2.1.1",
"hono": "^4.12.12",
"jsonrepair": "^3.13.3",
+ "lenis": "^1.3.25",
"lucide-react": "^1.8.0",
"motion": "^12.42.2",
"picomatch": "^4.0.4",
diff --git a/scripts/migrate.mjs b/scripts/migrate.mjs
index bff796f..07ae8b5 100644
--- a/scripts/migrate.mjs
+++ b/scripts/migrate.mjs
@@ -271,15 +271,6 @@ async function ensureModelCatalog() {
await query('ALTER TABLE model_configs ADD COLUMN IF NOT EXISTS provider_id UUID');
await query('ALTER TABLE model_configs ADD COLUMN IF NOT EXISTS model_name TEXT');
- await query('ALTER TABLE model_configs ALTER COLUMN rpm DROP NOT NULL');
- await query('ALTER TABLE model_configs ALTER COLUMN tpm DROP NOT NULL');
- await query('ALTER TABLE model_configs ALTER COLUMN rpd DROP NOT NULL');
- await query(`
- UPDATE model_configs
- SET rpm = NULL, tpm = NULL, rpd = NULL, updated_at = now()
- WHERE rpm = 1 AND tpm = 1 AND rpd = 1
- `);
-
await query(
`
UPDATE model_configs mc
@@ -317,14 +308,11 @@ async function ensureModelCatalog() {
await query(
`
- INSERT INTO model_configs (model_id, rpm, tpm, rpd, provider, provider_id, model_name, updated_at)
- SELECT $1, 10, 131072, 300, 'cloudflare', p.id, $1, now()
+ INSERT INTO model_configs (model_id, provider, provider_id, model_name, updated_at)
+ SELECT $1, 'cloudflare', p.id, $1, now()
FROM llm_providers p
WHERE p.name = 'Cloudflare'
ON CONFLICT (model_id) DO UPDATE SET
- rpm = EXCLUDED.rpm,
- tpm = EXCLUDED.tpm,
- rpd = EXCLUDED.rpd,
provider = EXCLUDED.provider,
provider_id = EXCLUDED.provider_id,
model_name = EXCLUDED.model_name,
@@ -335,14 +323,11 @@ async function ensureModelCatalog() {
await query(
`
- INSERT INTO model_configs (model_id, rpm, tpm, rpd, provider, provider_id, model_name, updated_at)
- SELECT '@cf/zai-org/glm-4.7-flash', 20, 131072, 600, 'cloudflare', p.id, '@cf/zai-org/glm-4.7-flash', now()
+ INSERT INTO model_configs (model_id, provider, provider_id, model_name, updated_at)
+ SELECT '@cf/zai-org/glm-4.7-flash', 'cloudflare', p.id, '@cf/zai-org/glm-4.7-flash', now()
FROM llm_providers p
WHERE p.name = 'Cloudflare'
ON CONFLICT (model_id) DO UPDATE SET
- rpm = EXCLUDED.rpm,
- tpm = EXCLUDED.tpm,
- rpd = EXCLUDED.rpd,
provider = EXCLUDED.provider,
provider_id = EXCLUDED.provider_id,
model_name = EXCLUDED.model_name,
@@ -352,8 +337,8 @@ async function ensureModelCatalog() {
await query(
`
- INSERT INTO model_configs (model_id, rpm, tpm, rpd, provider, provider_id, model_name, updated_at)
- SELECT 'gemma-4-31b-it', 15, 1000000, 1500, 'gemini', p.id, 'gemma-4-31b-it', now()
+ INSERT INTO model_configs (model_id, provider, provider_id, model_name, updated_at)
+ SELECT 'gemma-4-31b-it', 'gemini', p.id, 'gemma-4-31b-it', now()
FROM llm_providers p
WHERE p.name = 'Google'
ON CONFLICT (model_id) DO UPDATE SET
@@ -366,8 +351,8 @@ async function ensureModelCatalog() {
await query(
`
- INSERT INTO model_configs (model_id, rpm, tpm, rpd, provider, provider_id, model_name, updated_at)
- SELECT 'gemma-4-26b-a4b-it', 30, 1000000, 1500, 'gemini', p.id, 'gemma-4-26b-a4b-it', now()
+ INSERT INTO model_configs (model_id, provider, provider_id, model_name, updated_at)
+ SELECT 'gemma-4-26b-a4b-it', 'gemini', p.id, 'gemma-4-26b-a4b-it', now()
FROM llm_providers p
WHERE p.name = 'Google'
ON CONFLICT (model_id) DO UPDATE SET
diff --git a/src/client/app.css b/src/client/app.css
index b761a31..d06acd0 100644
--- a/src/client/app.css
+++ b/src/client/app.css
@@ -2,6 +2,10 @@
@import "tailwindcss";
+/* Make `dark:` utilities follow the app's `.dark` class (theme toggle)
+ instead of the OS `prefers-color-scheme` default. */
+@custom-variant dark (&:where(.dark, .dark *));
+
/* ─────────────────────────────────────────────────────
Easing tokens
───────────────────────────────────────────────────── */
@@ -903,8 +907,7 @@
gap: 0.625rem !important;
padding: 0.75rem 0.875rem !important;
border-radius: 0.625rem !important;
- border-width: 1px !important;
- border-style: solid !important;
+ border: none !important;
font-family: var(--font-sans) !important;
font-size: 0.8125rem !important;
line-height: 1.45 !important;
@@ -915,7 +918,6 @@
/* light defaults (overridden per-variant below) */
background: oklch(99.5% 0.004 115) !important;
- border-color: oklch(88% 0.008 115) !important;
color: oklch(15% 0.02 115) !important;
/* smooth entrance */
@@ -924,7 +926,6 @@
.dark .codra-toast {
background: oklch(13% 0.018 115) !important;
- border-color: oklch(22% 0.022 115) !important;
color: oklch(94% 0.006 115) !important;
box-shadow:
0 6px 24px oklch(0% 0 0 / 0.5),
@@ -983,64 +984,9 @@
background: oklch(28% 0.022 115) !important;
}
-/* ── SUCCESS ─────────────────────────────────────── */
-.codra-toast-success {
- background: oklch(98.5% 0.045 115) !important;
- border-color: oklch(82% 0.16 115) !important;
- color: oklch(28% 0.10 115) !important;
-}
-
-.dark .codra-toast-success {
- background: oklch(16% 0.08 115) !important;
- border-color: oklch(32% 0.14 115) !important;
- color: oklch(90% 0.18 115) !important;
-}
-
-.codra-toast-success .codra-toast-description {
- color: oklch(38% 0.10 115) !important;
- opacity: 0.85 !important;
-}
-
-.dark .codra-toast-success .codra-toast-description {
- color: oklch(72% 0.12 115) !important;
- opacity: 0.9 !important;
-}
-
-/* ── ERROR ───────────────────────────────────────── */
-.codra-toast-error {
- background: oklch(98.5% 0.03 25) !important;
- border-color: oklch(80% 0.14 25) !important;
- color: oklch(32% 0.14 25) !important;
-}
-
-.dark .codra-toast-error {
- background: oklch(15% 0.07 25) !important;
- border-color: oklch(35% 0.14 25) !important;
- color: oklch(85% 0.08 25) !important;
-}
-
-.codra-toast-error .codra-toast-description {
- color: oklch(42% 0.12 25) !important;
- opacity: 0.85 !important;
-}
-
-.dark .codra-toast-error .codra-toast-description {
- color: oklch(68% 0.10 25) !important;
- opacity: 0.9 !important;
-}
-
-/* ── LOADING ─────────────────────────────────────── */
-.codra-toast-loading {
- background: oklch(98% 0.004 115) !important;
- border-color: oklch(86% 0.010 115) !important;
- color: oklch(20% 0.020 115) !important;
-}
-
-.dark .codra-toast-loading {
- background: oklch(14% 0.020 115) !important;
- border-color: oklch(24% 0.025 115) !important;
- color: oklch(88% 0.008 115) !important;
-}
+/* ── SUCCESS / ERROR / LOADING ─────────────────────
+ Use the default toast text color instead of a
+ status tint (icon color already conveys status). */
/* spinner inherits accent color */
.codra-toast-loader svg {
@@ -1049,26 +995,18 @@
/* ── WARNING ─────────────────────────────────────── */
.codra-toast-warning {
- background: oklch(98.5% 0.04 65) !important;
- border-color: oklch(82% 0.13 65) !important;
- color: oklch(35% 0.12 65) !important;
+ color: oklch(35% 0.12 65) !important;
}
.dark .codra-toast-warning {
- background: oklch(16% 0.08 65) !important;
- border-color: oklch(35% 0.14 65) !important;
- color: oklch(82% 0.14 65) !important;
+ color: oklch(82% 0.14 65) !important;
}
/* ── INFO ────────────────────────────────────────── */
.codra-toast-info {
- background: oklch(98.5% 0.03 250) !important;
- border-color: oklch(80% 0.12 250) !important;
- color: oklch(30% 0.12 250) !important;
+ color: oklch(30% 0.12 250) !important;
}
.dark .codra-toast-info {
- background: oklch(15% 0.07 250) !important;
- border-color: oklch(33% 0.12 250) !important;
- color: oklch(80% 0.12 250) !important;
+ color: oklch(80% 0.12 250) !important;
}
diff --git a/src/client/components/features/job-detail/file-finding.tsx b/src/client/components/features/job-detail/file-finding.tsx
index c972f5e..c048610 100644
--- a/src/client/components/features/job-detail/file-finding.tsx
+++ b/src/client/components/features/job-detail/file-finding.tsx
@@ -23,7 +23,7 @@ export function FileFinding({ file }: FileFindingProps) {
-
+ {file.fileStatus === 'done' && }
{file.parsedComments.length > 0 && (
{file.parsedComments.length}
diff --git a/src/client/components/features/job-detail/job-findings-list.tsx b/src/client/components/features/job-detail/job-findings-list.tsx
index 084eb1f..a1b4b32 100644
--- a/src/client/components/features/job-detail/job-findings-list.tsx
+++ b/src/client/components/features/job-detail/job-findings-list.tsx
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { FileText } from 'lucide-react';
-import { cn } from '@client/lib/utils';
import type { JobDetail } from '@shared/schema';
import { reviewSeverities } from '@shared/schema';
+import { Tabs, TabsList, TabsTrigger } from '@client/components/motion/tabs';
import { FileFinding } from './file-finding';
import { CommentCard } from './comment-card';
import { severityConfig } from './constants';
@@ -24,22 +24,16 @@ export function JobFindingsList({ job }: JobFindingsListProps) {
View by
-
- {(['files', 'severity'] as const).map((view) => (
-
- ))}
-
+
setViewBy(v as 'files' | 'severity')} variant="segment">
+
+
+ Files
+
+
+ Severity
+
+
+
@@ -59,6 +53,30 @@ export function JobFindingsList({ job }: JobFindingsListProps) {
) : (
+ {job.files.some((f) => f.fileStatus === 'failed') && (
+
+ {/* Group header */}
+
+
+
+ Failed Files
+
+
+ {job.files.filter((f) => f.fileStatus === 'failed').length}
+
+
+ {/* Failed files list */}
+
+ {job.files.filter((f) => f.fileStatus === 'failed').map((file) => (
+
+ ))}
+
+
+ )}
+
{reviewSeverities.map((groupName) => {
const comments = job.files.flatMap((f) =>
f.parsedComments
diff --git a/src/client/components/features/job-detail/job-header.tsx b/src/client/components/features/job-detail/job-header.tsx
index 787ff00..6256049 100644
--- a/src/client/components/features/job-detail/job-header.tsx
+++ b/src/client/components/features/job-detail/job-header.tsx
@@ -1,16 +1,43 @@
+import { useState } from 'react';
import { Link } from 'react-router-dom';
-import { ChevronRight, ExternalLink, RotateCcw, Terminal } from 'lucide-react';
+import {
+ ChevronRight,
+ CircleStop,
+ ExternalLink,
+ Loader2,
+ RotateCcw,
+ Terminal,
+ Trash2,
+} from 'lucide-react';
import { Button } from '@client/components/ui/button';
+import { ConfirmDialog } from '@client/components/ui/confirm-dialog';
import { UpdatesEmailPrompt } from '@client/components/features/dashboard/updates-email-prompt';
import type { JobDetail } from '@shared/schema';
interface JobHeaderProps {
job: JobDetail;
- isRetrying: boolean;
- onRetry: () => void;
+ isRerunning: boolean;
+ isStopping: boolean;
+ isDeleting: boolean;
+ onRerun: () => void;
+ onStop: () => void;
+ onDelete: () => void;
}
-export function JobHeader({ job, isRetrying, onRetry }: JobHeaderProps) {
+export function JobHeader({
+ job,
+ isRerunning,
+ isStopping,
+ isDeleting,
+ onRerun,
+ onStop,
+ onDelete,
+}: JobHeaderProps) {
+ const [stopOpen, setStopOpen] = useState(false);
+ const [deleteOpen, setDeleteOpen] = useState(false);
+
+ const canStop = job.status === 'running' || job.status === 'queued';
+
return (
<>
@@ -43,24 +70,70 @@ export function JobHeader({ job, isRetrying, onRetry }: JobHeaderProps) {
-
-