From c19d549ae81220ead948a647c6c85811b0d670a3 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Fri, 22 May 2026 13:44:25 -0600 Subject: [PATCH] Show raw Jira status everywhere instead of mapping to Open/In Progress/Closed - Drop CHECK constraint on jira_tickets.status to allow any status string - Store raw Jira status directly in status column during sync (remove mapJiraStatusToLocal) - Remove VALID_TICKET_STATUSES validation on create/update endpoints - Remove separate Jira Status column from table (status IS the Jira status now) - Update frontend status badges to color-code dynamically based on status category - Update Open Tickets widget and CVE detail view to use isClosedStatus() helper - Make filter dropdown dynamic based on actual ticket statuses - Add migration script for dropping the constraint on other deployments --- backend/db-schema.sql | 2 +- .../drop_jira_status_check_constraint.js | 18 ++++++++ backend/routes/jiraTickets.js | 15 +++---- frontend/src/App.js | 44 +++++++++++++------ frontend/src/components/pages/JiraPage.js | 43 +++++++++++++----- 5 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 backend/migrations/drop_jira_status_check_constraint.js diff --git a/backend/db-schema.sql b/backend/db-schema.sql index bf1a631..7c8f419 100644 --- a/backend/db-schema.sql +++ b/backend/db-schema.sql @@ -126,7 +126,7 @@ CREATE TABLE IF NOT EXISTS jira_tickets ( ticket_key TEXT NOT NULL, url TEXT, summary TEXT, - status TEXT DEFAULT 'Open' CHECK (status IN ('Open', 'In Progress', 'Closed')), + status TEXT DEFAULT 'Open', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); diff --git a/backend/migrations/drop_jira_status_check_constraint.js b/backend/migrations/drop_jira_status_check_constraint.js new file mode 100644 index 0000000..1d10dcc --- /dev/null +++ b/backend/migrations/drop_jira_status_check_constraint.js @@ -0,0 +1,18 @@ +// Migration: Drop CHECK constraint on jira_tickets.status +// Allows storing raw Jira status strings (e.g. "Approval/Handoff", "Prioritizing") +// instead of mapping to the limited set of Open/In Progress/Closed. +// Idempotent — safe to run multiple times. + +const pool = require('../db'); + +async function run() { + console.log('[Migration] Dropping jira_tickets_status_check constraint...'); + await pool.query(`ALTER TABLE jira_tickets DROP CONSTRAINT IF EXISTS jira_tickets_status_check`); + console.log('✓ jira_tickets status CHECK constraint dropped (or did not exist)'); + await pool.end(); +} + +run().catch(err => { + console.error('Migration failed:', err.message); + process.exit(1); +}); diff --git a/backend/routes/jiraTickets.js b/backend/routes/jiraTickets.js index bb7511a..ffec557 100644 --- a/backend/routes/jiraTickets.js +++ b/backend/routes/jiraTickets.js @@ -18,7 +18,6 @@ const jiraApi = require('../helpers/jiraApi'); // Validation helpers const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/; -const VALID_TICKET_STATUSES = ['Open', 'In Progress', 'Closed']; function isValidCveId(cveId) { return typeof cveId === 'string' && CVE_ID_PATTERN.test(cveId); @@ -354,12 +353,11 @@ function createJiraTicketsRouter() { const jiraStatus = issue.fields.status ? issue.fields.status.name : null; const jiraSummary = issue.fields.summary || ticket.summary; - const localStatus = mapJiraStatusToLocal(jiraStatus); try { await pool.query( `UPDATE jira_tickets SET summary = $1, status = $2, jira_status = $3, last_synced_at = NOW(), updated_at = NOW() WHERE id = $4`, - [jiraSummary, localStatus, jiraStatus, ticket.id] + [jiraSummary, jiraStatus || 'Open', jiraStatus, ticket.id] ); results.synced++; } catch (dbErr) { @@ -435,11 +433,10 @@ function createJiraTicketsRouter() { const issue = result.data; const jiraStatus = issue.fields.status ? issue.fields.status.name : null; const jiraSummary = issue.fields.summary || ticket.summary; - const localStatus = mapJiraStatusToLocal(jiraStatus); await pool.query( `UPDATE jira_tickets SET summary = $1, status = $2, jira_status = $3, last_synced_at = NOW(), updated_at = NOW() WHERE id = $4`, - [jiraSummary, localStatus, jiraStatus, id] + [jiraSummary, jiraStatus || 'Open', jiraStatus, id] ); logAudit({ @@ -563,8 +560,8 @@ function createJiraTicketsRouter() { if (summary && (typeof summary !== 'string' || summary.length > 500)) { return res.status(400).json({ error: 'Summary must be under 500 characters.' }); } - if (status && !VALID_TICKET_STATUSES.includes(status)) { - return res.status(400).json({ error: `Status must be one of: ${VALID_TICKET_STATUSES.join(', ')}` }); + if (status && typeof status !== 'string') { + return res.status(400).json({ error: 'Status must be a string.' }); } const ticketStatus = status || 'Open'; @@ -632,8 +629,8 @@ function createJiraTicketsRouter() { if (summary !== undefined && summary !== null && (typeof summary !== 'string' || summary.length > 500)) { return res.status(400).json({ error: 'Summary must be under 500 characters.' }); } - if (status !== undefined && !VALID_TICKET_STATUSES.includes(status)) { - return res.status(400).json({ error: `Status must be one of: ${VALID_TICKET_STATUSES.join(', ')}` }); + if (status !== undefined && typeof status !== 'string') { + return res.status(400).json({ error: 'Status must be a string.' }); } const fields = []; diff --git a/frontend/src/App.js b/frontend/src/App.js index 3f84632..2834869 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -25,6 +25,22 @@ import './App.css'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; +// Determine if a Jira status represents a "closed/done" state +function isClosedStatus(status) { + if (!status) return false; + const lower = status.toLowerCase(); + return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s)); +} + +function getTicketStatusColor(status) { + if (!status) return '#F59E0B'; + if (isClosedStatus(status)) return '#10B981'; + const lower = status.toLowerCase(); + if (['open', 'to do', 'backlog', 'new'].some(s => lower === s)) return '#F59E0B'; + // Everything else (in progress, approval, prioritizing, etc.) gets blue/purple + return '#0EA5E9'; +} + // ============================================ // INLINE STYLES - NUCLEAR OPTION FOR VISIBILITY // ============================================ @@ -1072,7 +1088,7 @@ export default function App() {
Open Tickets
-
{jiraTickets.filter(t => t.status !== 'Closed').length}
+
{jiraTickets.filter(t => !isClosedStatus(t.status)).length}
@@ -1475,6 +1491,9 @@ export default function App() { + {ticketForm.status && !['Open', 'In Progress', 'Closed'].includes(ticketForm.status) && ( + + )}
@@ -1544,6 +1563,9 @@ export default function App() { + {ticketForm.status && !['Open', 'In Progress', 'Closed'].includes(ticketForm.status) && ( + + )}
@@ -2096,15 +2118,11 @@ export default function App() { {ticket.summary && {ticket.summary}} - + {ticket.status}
@@ -2220,12 +2238,12 @@ export default function App() {
- {jiraTickets.filter(t => t.status !== 'Closed').length} + {jiraTickets.filter(t => !isClosedStatus(t.status)).length}
Active
- {jiraTickets.filter(t => t.status !== 'Closed').slice(0, 10).map(ticket => ( + {jiraTickets.filter(t => !isClosedStatus(t.status)).slice(0, 10).map(ticket => (
{ticket.summary}
}
- + {ticket.status}
))} - {jiraTickets.filter(t => t.status !== 'Closed').length === 0 && ( + {jiraTickets.filter(t => !isClosedStatus(t.status)).length === 0 && (

No open tickets

diff --git a/frontend/src/components/pages/JiraPage.js b/frontend/src/components/pages/JiraPage.js index d49f691..356d2b2 100644 --- a/frontend/src/components/pages/JiraPage.js +++ b/frontend/src/components/pages/JiraPage.js @@ -134,8 +134,28 @@ const STATUS_COLORS = { 'Open': '#F59E0B', 'In Progress': '#0EA5E9', 'Closed': '#10B981', + 'Done': '#10B981', + 'Resolved': '#10B981', + 'Approval/Handoff': '#8B5CF6', + 'Prioritizing': '#0EA5E9', + 'In Review': '#0EA5E9', + 'In Development': '#0EA5E9', + 'In Testing': '#0EA5E9', }; +// Determine if a status represents a "closed/done" state +function isClosedStatus(status) { + if (!status) return false; + const lower = status.toLowerCase(); + return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s)); +} + +function getStatusColor(status) { + if (STATUS_COLORS[status]) return STATUS_COLORS[status]; + if (isClosedStatus(status)) return '#10B981'; + return '#F59E0B'; +} + const SOURCE_CONTEXT_CONFIG = { cve: { label: 'CVE', color: '#0EA5E9' }, archer: { label: 'Archer', color: '#8B5CF6' }, @@ -307,7 +327,7 @@ export default function JiraPage() { ticket_key: issue.key, url: jiraUrl, summary: issue.summary || '', - status: issue.status === 'Open' || issue.status === 'In Progress' || issue.status === 'Closed' ? issue.status : 'Open', + status: issue.status || 'Open', }), }); const data = await res.json(); @@ -470,9 +490,8 @@ export default function JiraPage() { const counts = { total: tickets.length, - open: tickets.filter(t => t.status === 'Open').length, - inProgress: tickets.filter(t => t.status === 'In Progress').length, - closed: tickets.filter(t => t.status === 'Closed').length, + open: tickets.filter(t => !isClosedStatus(t.status)).length, + closed: tickets.filter(t => isClosedStatus(t.status)).length, }; @@ -549,7 +568,6 @@ export default function JiraPage() { {[ { label: 'Total', value: counts.total, color: '#0EA5E9' }, { label: 'Open', value: counts.open, color: '#F59E0B' }, - { label: 'In Progress', value: counts.inProgress, color: '#0EA5E9' }, { label: 'Closed', value: counts.closed, color: '#10B981' }, ].map(s => (
@@ -586,9 +604,9 @@ export default function JiraPage() { onChange={e => setFilterStatus(e.target.value)} > - - - + {[...new Set(tickets.map(t => t.status).filter(Boolean))].sort().map(s => ( + + ))}