From d364c25f6b1743a9ef08234fd822d98a654c2196 Mon Sep 17 00:00:00 2001
From: Lupita Bot <263059171+lupita-hom@users.noreply.github.com>
Date: Wed, 4 Mar 2026 09:27:24 -0600
Subject: [PATCH 1/2] fix: handle partial workflow_job records and avoid ghost
queued rows; show '#-' when run number missing
---
.../features/workflows/WorkflowHistory.jsx | 2 +-
server/src/services/workflowService.js | 20 ++++++++++++++-----
2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/client/src/features/workflows/WorkflowHistory.jsx b/client/src/features/workflows/WorkflowHistory.jsx
index 04bceaf..b29aadc 100644
--- a/client/src/features/workflows/WorkflowHistory.jsx
+++ b/client/src/features/workflows/WorkflowHistory.jsx
@@ -403,7 +403,7 @@ const WorkflowHistory = () => {
/>
- #{workflow.run.number}
+ {workflow.run.number ? `#${workflow.run.number}` : '#-'}
{workflow.run.head_branch || '-'}
diff --git a/server/src/services/workflowService.js b/server/src/services/workflowService.js
index d613d24..f42101d 100644
--- a/server/src/services/workflowService.js
+++ b/server/src/services/workflowService.js
@@ -53,10 +53,16 @@ export const processWorkflowRun = async (payload) => {
status: run.status
});
- // Find existing run to preserve any existing labels if none provided
+ // Find existing run to preserve any existing labels and jobs if none provided
const existingRun = await WorkflowRun.findOne({ 'run.id': run.id });
- if (existingRun && !workflowRunData.run.labels.length && existingRun.run.labels?.length) {
- workflowRunData.run.labels = existingRun.run.labels;
+ if (existingRun) {
+ if (!workflowRunData.run.labels.length && existingRun.run.labels?.length) {
+ workflowRunData.run.labels = existingRun.run.labels;
+ }
+ // Preserve existing jobs — workflow_run events don't carry job data
+ if (existingRun.jobs?.length) {
+ workflowRunData.jobs = existingRun.jobs;
+ }
}
// Use findOneAndUpdate with upsert to either update existing or create new
@@ -312,12 +318,16 @@ export const processWorkflowJobEvent = async (payload) => {
return workflowRun;
} else {
- // If the run doesn't exist, create a new one with the job
+ // If the run doesn't exist, create a new one with the job.
+ // Note: workflow_job payloads lack run-level fields (head_branch, event, etc.),
+ // so we mark this as a partial record. The workflow_run event will fill in the rest.
+ workflowRunData.run.head_branch = workflow_job.head_branch || null;
+ workflowRunData.run.event = null; // Not available in workflow_job payload
workflowRunData.jobs = [updatedJob];
const workflowRun = await WorkflowRun.findOneAndUpdate(
{ 'run.id': workflow_job.run_id },
- workflowRunData,
+ { $setOnInsert: workflowRunData },
{
new: true,
upsert: true,
From d171f7bebadf7e86110284db65e667beac28041a Mon Sep 17 00:00:00 2001
From: Lupita Bot <263059171+lupita-hom@users.noreply.github.com>
Date: Wed, 4 Mar 2026 10:12:12 -0600
Subject: [PATCH 2/2] feat: add delete button for workflow runs to remove
orphan/ghost records
- Add DELETE /api/workflow-runs/:id endpoint
- Add Delete button (red) next to Sync in WorkflowHistory
- Confirm dialog before deletion
- Real-time updates via workflowDeleted socket event
- Allows cleaning up ghost Queued records that are stuck in DB
---
client/src/api/apiService.js | 10 ++++
client/src/api/socketService.js | 8 ++++
.../features/workflows/WorkflowHistory.jsx | 47 +++++++++++++++++++
server/src/controllers/workflowController.js | 20 ++++++++
server/src/routes/api.js | 3 ++
5 files changed, 88 insertions(+)
diff --git a/client/src/api/apiService.js b/client/src/api/apiService.js
index 1b161e5..e015c01 100644
--- a/client/src/api/apiService.js
+++ b/client/src/api/apiService.js
@@ -80,6 +80,16 @@ const apiService = {
}
},
+ deleteWorkflowRun: async (id) => {
+ try {
+ const response = await api.delete(`${API_URL}/workflow-runs/${id}`);
+ return response.data.data;
+ } catch (error) {
+ console.error(`Error deleting workflow run ${id}:`, error);
+ throw error;
+ }
+ },
+
// Sync all workflow runs for a repository
syncWorkflowRuns: async (repoName) => {
try {
diff --git a/client/src/api/socketService.js b/client/src/api/socketService.js
index 249fcfc..47ef3ef 100644
--- a/client/src/api/socketService.js
+++ b/client/src/api/socketService.js
@@ -189,6 +189,13 @@ export const setupSocketListeners = (callbacks) => {
debouncedJobsUpdate(data);
});
+ socket.on('workflowDeleted', (data) => {
+ console.log('Received workflowDeleted event:', data);
+ if (callbacks.onWorkflowDeleted) {
+ callbacks.onWorkflowDeleted(data);
+ }
+ });
+
// Setup the queue time monitoring
const queueMonitorInterval = setInterval(() => checkQueuedWorkflows(alertConfig, callbacks.onLongQueuedWorkflow), 30000); // Check every 30 seconds
@@ -200,6 +207,7 @@ export const setupSocketListeners = (callbacks) => {
socket.off('workflowUpdate');
socket.off('workflow_update');
socket.off('workflowJobsUpdate');
+ socket.off('workflowDeleted');
clearInterval(queueMonitorInterval);
lastUpdateTimes.clear();
queuedWorkflows.clear();
diff --git a/client/src/features/workflows/WorkflowHistory.jsx b/client/src/features/workflows/WorkflowHistory.jsx
index b29aadc..e3f9ce3 100644
--- a/client/src/features/workflows/WorkflowHistory.jsx
+++ b/client/src/features/workflows/WorkflowHistory.jsx
@@ -42,6 +42,7 @@ const WorkflowHistory = () => {
const [error, setError] = useState(null);
const [stats, setStats] = useState(null);
const [syncingRun, setSyncingRun] = useState(null);
+ const [deletingRun, setDeletingRun] = useState(null);
const calculateStats = (runs) => {
if (!runs.length) return null;
@@ -143,6 +144,25 @@ const WorkflowHistory = () => {
}
};
+ const handleDeleteRun = async (e, runId) => {
+ e.stopPropagation(); // Prevent row click
+ if (!window.confirm('Are you sure you want to delete this workflow run record?')) return;
+ try {
+ setDeletingRun(runId);
+ await apiService.deleteWorkflowRun(runId);
+ // Remove from local state immediately
+ setWorkflowRuns(prev => {
+ const updated = prev.filter(w => w.run.id !== runId);
+ setStats(calculateStats(updated));
+ return updated;
+ });
+ } catch (error) {
+ console.error('Error deleting workflow run:', error);
+ } finally {
+ setDeletingRun(null);
+ }
+ };
+
const fetchWorkflowHistory = async () => {
try {
setLoading(true);
@@ -192,6 +212,13 @@ const WorkflowHistory = () => {
return updated;
});
}
+ },
+ onWorkflowDeleted: ({ runId }) => {
+ setWorkflowRuns(prev => {
+ const updated = prev.filter(w => w.run.id !== runId);
+ setStats(calculateStats(updated));
+ return updated;
+ });
}
});
@@ -475,6 +502,26 @@ const WorkflowHistory = () => {
'Sync'
)}
+
diff --git a/server/src/controllers/workflowController.js b/server/src/controllers/workflowController.js
index 4ebd0af..56eee81 100644
--- a/server/src/controllers/workflowController.js
+++ b/server/src/controllers/workflowController.js
@@ -255,6 +255,26 @@ export const getWorkflowRunById = async (req, res) => {
}
};
+export const deleteWorkflowRun = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const workflowRun = await WorkflowRun.findOneAndDelete({ 'run.id': parseInt(id) });
+
+ if (!workflowRun) {
+ return errorResponse(res, 'Workflow run not found', 404);
+ }
+
+ // Notify connected clients so the UI updates in real-time
+ if (req.io) {
+ req.io.emit('workflowDeleted', { runId: parseInt(id) });
+ }
+
+ return successResponse(res, { deleted: true, runId: parseInt(id) }, 'Workflow run deleted');
+ } catch (error) {
+ return errorResponse(res, 'Error deleting workflow run', 500, error);
+ }
+};
+
export const syncWorkflowRun = async (req, res) => {
try {
const { id } = req.params;
diff --git a/server/src/routes/api.js b/server/src/routes/api.js
index 8d6e603..7ee23c3 100644
--- a/server/src/routes/api.js
+++ b/server/src/routes/api.js
@@ -39,6 +39,9 @@ router.get('/workflow-runs/queued', workflowController.getQueuedWorkflows);
// Get workflow run by ID
router.get('/workflow-runs/:id', workflowController.getWorkflowRunById);
+// Delete a workflow run by ID
+router.delete('/workflow-runs/:id', workflowController.deleteWorkflowRun);
+
// Sync workflow run
router.post('/workflow-runs/:id/sync', workflowController.syncWorkflowRun);