- 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
PostgreSQL DATE columns return JS Date objects which serialize to ISO
timestamps (e.g. 2025-05-22T00:00:00.000Z). The CalendarWidget expects
plain YYYY-MM-DD strings for its date key lookup. Added formatDate()
helper to normalize due_date and last_found_on before sending the
API response.
The View button for documents was constructing the href as
API_HOST + file_path which resolved to http://localhost:3001/...
Since the frontend is served from the same Express server, this
should be a relative path. Users' browsers don't have localhost:3001
running, so the link was broken for anyone not on the server itself.
Library documents from the knowledge base were not checked against
the allowed file extensions before being sent to Ivanti. If a doc
had an unsupported type (e.g. .msg, .eml), Ivanti would reject the
entire workflow with a 400. Now validates library docs the same way
as local uploads and returns a clear error naming the offending file.
Allowed: .pdf, .png, .jpg, .jpeg, .gif, .doc, .docx, .xlsx, .csv, .txt, .zip
When the Ivanti API returns a non-success status, the error message
now includes the actual response body from Ivanti instead of just
the HTTP status code. This makes troubleshooting much easier since
you can see what Ivanti rejected (e.g. invalid field, too many
attachments, malformed request).
Shows a notification with the ticket key (e.g. STEAM-2672) as a
clickable link to the Jira issue. Auto-dismisses after 8 seconds.
Errors are already shown inline in the ConsolidationModal.
The multi-select consolidated Jira ticket feature was built into a
standalone page that doesn't exist. This wires it into the actual
QueuePanel slide-out where users work. Adds a '+ Jira (N)' button
to the footer action bar that opens the ConsolidationModal when 2+
items are selected, or the single-item Jira modal for 1 item.
Select multiple queue items and create a single consolidated Jira ticket
with aggregated summary and description. Adds multi-select mode with
checkboxes, floating action bar, consolidation modal, and junction table
to track which queue items contributed to each ticket.
- Migration: jira_ticket_queue_items junction table
- POST /api/jira-tickets/:id/queue-items endpoint
- GET /api/ivanti/todo-queue/ticket-links endpoint
- ConsolidationModal component with aggregation logic
- IvantiTodoQueuePage with selection mode and ticket link badges
- Pure utility functions for summary/description generation
- 34 tests passing (backend + frontend)
- Add add_jira_sync_columns_pg.js migration (jira_id, jira_status, last_synced_at, created_by)
- Register in run-all.js before the flexible creation migration
- Replace all generic 'Internal server error' with actual err.message in jiraTickets routes
- Users and admins can now see the real failure reason instead of a useless generic message
- Pass through actual Jira error details instead of generic 'Jira API error'
- Parse errorMessages and errors from Jira response for human-readable display
- Make cve_id and vendor optional on local POST /api/jira-tickets (for Save to Dashboard)
- Update getIssue comment for clarity (logic unchanged — JQL search per compliance spec)
- Replace issue type text input with dropdown of STEAM project types (Story default)
- Add Save to Dashboard button on lookup results to link existing Jira tickets locally
- Make cve_id and vendor optional on local POST /api/jira-tickets endpoint
- Fix: use normalized values in local ticket INSERT query
Make CVE ID and Vendor optional when creating Jira tickets. Add source_context
field to track ticket origin (cve, archer, ivanti_queue, email, manual).
- Migration: drop NOT NULL on cve_id/vendor, add source_context column with CHECK
- Backend: update create/update/get endpoints for optional fields and source_context
- Frontend: update creation modal with optional labels and source context dropdown
- Add Create Jira Ticket action from Ivanti queue (pre-populates from finding)
- Add Create Jira Ticket action from Archer detail view (pre-populates from ticket)
- Add source context badge column, filter dropdown, and search to ticket list
Flip stacked bar chart so non-compliant (orange) renders on top and
compliant (blue) on bottom for better visual emphasis.
Use the file's report_date for compliance_snapshots month instead of
the current date, so historical uploads land in the correct monthly
bucket. Also fix rollback to delete the correct month's snapshot.
Remove cve-frontend systemd service ( Express serves theredundant
built frontend on port 3001).
Devices appearing in multiple verticals were counted multiple times,
causing non_compliant > totalAssets and negative compliance percentages.
Deduplicate by hostname before passing to the forecast helper.
- Fix Date object handling for resolution_date from PostgreSQL
- Fix totalAssets using per-metric summary (vcl_multi_vertical_summary)
instead of vertical-level compliance_snapshots total_devices
- Fix duplicate current month in chart (forecast starts from next month)
- Fix multi-vertical metrics summing across all relevant verticals
- Fix bar stacking: orange (non-compliant) on bottom, blue (compliant)
on top, both sharing same baseline (stacked to total)
- Add fill props to Bar components for correct legend colors
- Backfill historical snapshots with per-metric totalAssets
New feature: combined historical + forecast burndown chart with metric
selector on the CCP Metrics page. Shows stacked bars (total assets vs
non-compliant) with a compliance percentage trend line. A bold divider
separates actual historical data from projected future remediation.
Forecast assumes constant asset count and on-schedule remediation plans.
Backend:
- computeMetricForecastBurndown helper in vclHelpers.js (pure function)
- GET /api/compliance/vcl-multi/metrics-list endpoint
- GET /api/compliance/vcl-multi/metric/:metricId/forecast-burndown endpoint
Frontend:
- MetricSelector dropdown with device counts per metric
- ForecastBurndownChart using recharts ComposedChart (Bar + Line + ReferenceLine)
- Forecast bars render at 50% opacity to distinguish from actuals
- Race condition handling for rapid metric switching
- Queue panel width increased from 420px to 600px
Closes#18
The project filter was intentionally removed from searchIssuesByKeys() to
fix cross-project ticket sync. Update the property test to no longer assert
the presence of 'project =' in the generated JQL.
CCPMetricsPage called isEditor() which does not exist in AuthContext.
Admin users were unaffected due to JS short-circuit evaluation on
isAdmin() || isEditor(). Standard_User accounts hit TypeError because
isEditor was undefined.
Replaced isEditor() with canWrite() which is the correct auth helper
for write-capable users (Admin + Standard_User).
Closes#15
Recharts PieChart throws internally when all data segments are zero.
Guard against this by rendering a friendly message instead of passing
all-zero data to the chart component.
Affects users whose vertical data has no non-compliant items.
- Feedback modal now supports up to 3 image attachments (PNG/JPG/GIF/WebP, 5MB
each) with thumbnail previews. Images are uploaded to GitLab project uploads
and embedded as markdown in the issue description.
- New webhook endpoint (POST /api/webhooks/gitlab) receives issue close events,
parses the submitter from the description, looks up their email, and sends a
Webex DM via the Patches O'Houlihan bot.
- New helper: backend/helpers/webexBot.js (fire-and-forget DM sender).
- Requires WEBEX_BOT_TOKEN and GITLAB_WEBHOOK_SECRET in backend/.env.
Deduplicate (hostname, metric_id) rows across verticals using DISTINCT ON in
GET /items, GET /items/:hostname, GET /vcl/stats (heavy-hitters + forecast),
GET /mttr, and persistUpload() snapshot block. Add defensive groupByHostname
Set and hostname_status CTE for snapshot classification.
Includes 38 property-based tests (11 exploration + 27 preservation) covering
all six affected sites.
Closes#13
deploy-staging and deploy-production now parse #N references from the commit
message and post a deployment comment on each referenced GitLab issue with a
link to the pipeline. Requires GITLAB_PAT CI/CD variable (see steering docs).
Aggregate /trends, /top-recurring, /category-trend by report_date instead of
per-upload row. Add sibling-upload disclosure to /summary. Filter persistUpload
snapshot query by the upload's vertical to prevent cross-vertical contamination.
Fixes GitLab #12 (reported by nkapur — STEAM active findings chart showed 3
entries for 5/11 after uploading three vertical data sets for that date).
Includes 30 property-based tests covering bug condition and preservation.
Single-file Node.js CLI that orchestrates the full setup lifecycle:
- Interactive env var configuration with validation and smart defaults
- Postgres provisioning via Docker Compose with readiness polling
- Schema initialization (psql with docker exec fallback)
- npm dependency installation with 120s timeout
- Optional SQLite-to-Postgres data migration with retry logic
- Frontend build with smart skip on reconfiguration
Includes 84 tests: 50 property-based (fast-check) covering 19 correctness
properties, and 34 integration tests for filesystem and parsing flows.
1. History entries saved at the same time by the same user now display
as a single grouped entry (resolution date + remediation plan together)
2. Removed '(optional)' from the change reason placeholder — engineers
should treat it as expected, even though the backend allows empty
3. Save button now saves both resolution date AND remediation plan in one
call (removed the onBlur auto-save on the date field) so they share
a timestamp and group correctly in history
New table compliance_item_history stores an append-only audit trail of
changes to resolution_date and remediation_plan. The current values remain
on compliance_items for fast VCL reporting queries (no double-counting).
Backend:
- Migration: creates compliance_item_history with indexes
- PATCH /items/:hostname/metadata: records old→new in history before updating,
accepts optional change_reason field (max 500 chars)
- GET /items/:hostname: returns history array (last 10 entries, newest first)
- POST /vcl/bulk-commit: records history for each changed field per hostname
Frontend:
- ComplianceDetailPanel: added change reason input below Save button
- Added Change History section showing field changes with timestamps,
usernames, old→new values, and reasons
- Re-fetches detail after save to show updated history immediately
Tests updated to match new transaction-based PATCH flow.
The /summary endpoint was fetching the most recent upload regardless of
vertical, which on dev was a PRDCT_VSO multi-vertical upload. Now it
looks for AEO uploads (vertical IS NULL) first, then falls back to the
NTS_AEO multi-vertical upload.
The /items endpoint now includes items from both vertical IS NULL and
vertical = 'NTS_AEO' so the AEO compliance page shows devices uploaded
through either flow.
Replaced the large flex-wrap button cards with a tight CSS grid of compact
cells (130px min). Each cell shows metric ID, current %, and NC count only.
Category text and target removed to reduce noise.
Capped to top 8 metrics by default with a 'Show all N' toggle for the rest.
Removes visual clutter while keeping the data accessible.
Clicking the Non-Compliant card on the CCP Metrics overview now toggles a
panel of metric buttons below it, each showing the metric ID, category,
non-compliant count, and compliance % vs target. Styled like the compliance
page's MetricHealthCard pattern.
Backend: added metric_breakdown to the /stats response — aggregated
cross-vertical metric totals (ALL: rows only, grouped by metric_id).
Also updated tech steering file to document the single-port Express
architecture and the requirement to run npm run build after frontend changes.
Clicking a metric now shows a sub-team breakdown page with totals per team
(compliant, non-compliant, total, %) instead of jumping directly to a flat
device list. Clicking a sub-team then shows the device list filtered to
that team only.
Navigation flow: Overview → Vertical → Metric (sub-team totals) → Team (devices)
Backend: added optional ?team= query param to the device list endpoint for
filtered queries.
Frontend: added MetricSubTeamView component with metric-level stats bar and
clickable sub-team table. Updated navigation state to include selectedTeam.
Also updated design brief to reflect the new drill-down hierarchy.
Backend: restructured /vertical/:code/metrics endpoint to return metrics
with nested sub_teams arrays. Each metric now has the ALL: rollup as the
primary row and individual team breakdowns (ACCESS-OPS, STEAM, etc.) as
sub_teams. Also returns a teams array for the filter UI.
Frontend: VerticalDetailView now supports two interaction modes:
- Expand/collapse: click the arrow on any metric row to reveal sub-team
breakdown inline (teal-highlighted rows beneath the parent)
- Team filter: click a team button to filter the entire table to show
only that team's numbers per metric
Both modes avoid double-counting by using the ALL: rollup for totals
and only showing sub-team data as supplementary detail.
The Summary sheet in each vertical spreadsheet contains both sub-team rows
(ACCESS-OPS, STEAM, INTELDEV, etc.) AND a rollup row (ALL: NTS-AEO) per
metric. The rollup row already includes all sub-team totals, so summing
all rows was double-counting every device.
Fixed in three places:
- GET /stats endpoint: added AND team LIKE 'ALL:%' filter
- persistMultiVerticalUpload snapshot creation: only sum ALL: entries
- GET /vertical/:code/metrics category aggregation: only use ALL: rows
Also ran a one-time data fix to correct existing compliance_snapshots.
The compliance_items table only contains non-compliant devices (detail
sheet rows). Compliant devices are never inserted — they only exist in
the Summary sheet totals. This caused Compliant to show 0 and
Compliance % to show 0% for all verticals.
Fix: stats endpoint now reads from vcl_multi_vertical_summary (parsed
Summary sheet data) for total/compliant/non-compliant counts. Snapshot
creation also uses summary data for accurate trend charting.
The compliance_items table is still used for:
- Donut chart (blocked vs in-progress based on resolution_date)
- Burndown forecast (devices with/without resolution dates)
- Device drill-down (actual non-compliant device list)