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
This commit is contained in:
Jordan Ramos
2026-05-22 13:44:25 -06:00
parent 2edf6228ff
commit c19d549ae8
5 changed files with 87 additions and 35 deletions

View File

@@ -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()
);

View File

@@ -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);
});

View File

@@ -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 = [];