The pg driver returns PostgreSQL DATE columns as ISO datetime strings
(e.g. '2026-07-03T00:00:00.000Z'). The formatResolutionDate helper was
strictly matching YYYY-MM-DD only, so these were classified as 'invalid'.
Now the helper extracts the date prefix from ISO datetime strings before
validating, correctly classifying them as 'set' with the YYYY-MM-DD value.
Updated the property test filter and added an example test for the case.
Add a natural-sort comparator for metric IDs (e.g. 2.3.6i, 5.2.6, 10.1.1)
and apply it to the metric breakdown cards, the vertical detail table, and
the forecast burndown metric dropdown. Metrics now appear in ascending
numerical order instead of arbitrary API response order.
Closes#24
The Resolution Date, Remediation Plan, and Apply To Metrics sections
now render immediately after Failing Metrics in the sidebar instead of
after Resolved Metrics and History — no more scrolling past unrelated
sections to reach the edit fields.
The date input also gains colorScheme: 'dark' so the native browser
calendar picker renders with light text on a dark background, fixing
the black-on-dark-blue readability issue.
Closes#21Closes#22
The host list on the compliance page showed stale resolution date and
remediation plan values after editing them in the detail sidebar, until
an unrelated refresh (filter, team, or tab change) ran. handleSaveMetadata
re-fetched only the panel's own detail and never notified the parent.
Add an onMetadataSaved callback invoked after a successful metadata PATCH
and wire it to the existing list refresh in CompliancePage, mirroring the
onNoteAdded pattern. The list now reflects saved changes immediately.
Closes#23
Add a read-only estimated resolution date line at the top of each
noncompliant metric's section in the asset sidebar, sourced from that
metric's own resolution_date. Formats valid dates as YYYY-MM-DD and
shows placeholders for unset and invalid dates. Resolved metrics are
unaffected and the existing editable Resolution Date field is unchanged.
Date classification is isolated in a pure helper (frontend/src/utils/
resolutionDate.js) covered by example and fast-check property tests,
with render and interaction tests for the sidebar.
Closes#20
Replace inline CARD action form with a centered modal that:
- Fetches and displays the full CARD owner record (confirmed,
unconfirmed, candidates, declined teams with scores/sources)
- Shows queue item info (hostname, IP, finding, CVEs)
- Lets user switch between Confirm/Decline/Redirect actions
- Pre-fills team dropdowns from the actual owner data
- Shows CARD API errors inline with full detail
Add GET /api/card/owner-lookup/:ip endpoint that resolves a bare
IP to a CARD asset ID and returns the structured owner record.
Show actual CARD API error messages (e.g., 'Cannot redirect asset
because Team is neither confirmed nor pending owner') instead of
generic 'Redirect failed.' or 'confirm failed.' messages.
Also auto-select IPV4_ADDRESS, EQUIP_NAME, and RESPONSIBLE_TEAM
columns by default in the Loader Modal for better initial UX.
Show clear error message when a queue item has no IP address
instead of sending null to the backend. Items without IPs cannot
be resolved to CARD asset IDs.
The 'Loader' button appears in the queue panel footer alongside
the existing + Jira, Delete, and Clear Completed buttons. It's
visible whenever CARD/GRANITE/DECOM items exist in the queue.
Clicking it opens the LoaderModal pre-populated with those items'
IPs and hostnames. If specific items are selected, only those are
passed; otherwise all CARD/GRANITE/DECOM items are included.
Implement the Granite Team_Device Loader xlsx export feature:
- Add graniteLoaderConfig.js with all 41 columns, groupings, and
operation-type requirements (Change/Add/Delete/Move)
- Add graniteLoaderExport.js for client-side xlsx generation using
the xlsx library
- Add LoaderModal component with operation type selection, column
checkboxes, bulk defaults with per-row overrides, editable preview
table, CARD enrichment integration, and standalone paste-IPs mode
- Add POST /api/card/enrich-batch endpoint for batch IP lookup in
CARD returning EQUIP_INST_ID, hostname, site, ASN, team
- Integrate 'Generate Loader Sheet' button in Ivanti Queue floating
action bar (visible when CARD/GRANITE/DECOM items selected)
- Add card-connectivity-test.js script for verifying CARD API access
The burndown chart uses live compliance_items data (updates as
remediation plans and resolution dates are entered), while the
metrics overview table shows the snapshot from the last uploaded
report. Add visual badges to clarify this distinction:
- Burndown chart: green 'LIVE' badge
- Metrics overview: grey 'LAST REPORT' badge
Expand VENDOR_PROJECT_KEYS to include all vendor projects: AA_ADTRAN,
AA_ADVA, AA_CASA, AA_CISCO, AACOMMSCOP, AA_COMMSCOP, AA_HARMONI,
AA_JUNIPER, AA_VECIMA, AA_VIAVI. Both AACOMMSCOP and AA_COMMSCOP
variants are included for safety.
Update property tests to exercise the full vendor key list instead of
only AA_VECIMA. Update full-reference-manual.md with vendor-specific
issue type dropdown documentation.
When the Project Key field contains a vendor project key (e.g. AA_VECIMA),
the Issue Type dropdown switches from STEAM types (Story, Epic, Program,
Project, Reservation, Automation Maintenance) to vendor types (Epic, Story,
Task, Defect, Production Defect/Incident Fix, New Feature, Spike, Release
Candidate, Documentation).
- Add VENDOR_PROJECT_KEYS, VENDOR_ISSUE_TYPES, STEAM_ISSUE_TYPES constants
- Add isVendorProject() and getIssueTypesForProject() pure functions
- Update JiraPage modal with context-aware dropdown and reset on switch
- Update Ivanti queue modal with project_key and issue_type fields
- Add property-based tests for determination logic and state transitions
Normalize the date in groupByHostname() to handle PostgreSQL Date objects,
and add .slice(0,10) in the frontend render as a safety net. Prevents the
full ISO timestamp (2026-05-15T00:00:00.000Z) from displaying in the table.
Make the INVENTORY and vendor group headers in the QueuePanel (slide-out
side panel) clickable to collapse/expand their contents. Adds a chevron
indicator showing collapse state. All sections start expanded by default.
Group queue items into a hybrid layout: Inventory section (CARD/GRANITE/DECOM)
at top, then vendor-grouped sections for FP/Archer items sorted alphabetically.
Each section header is clickable to collapse/expand with chevron indicators.
- Extract grouping logic into reusable utility (queueGrouping.js)
- Add collapse state management (all sections expanded by default)
- Preserve cross-section multi-select, floating action bar, ticket badges
- Add 5 property-based tests covering grouping correctness, ordering,
empty section omission, count accuracy, and selection independence
Display raw counts alongside percentages in the compliance metric health
cards, e.g. '67% (4/6)' instead of just '67%'. Data was already available
in the summary JSON (compliant, total fields) — just not rendered.
Closes#16
Per-metric remediation plan scoping (GitLab issue #19):
- Add metric_id column to compliance_item_history table (migration)
- Extend PATCH /items/:hostname/metadata to accept metric_id/metric_ids
for targeting specific metrics instead of all active items
- Add MetricChipSelector UI in detail panel for choosing which metrics
to apply resolution_date and remediation_plan changes to
- Display per-metric labels (MetricChip or 'All metrics') on history entries
- Backward compatible: omitting metric_ids preserves hostname-level behavior
CI/CD pipeline improvements:
- Add migration idempotency integration test (runs against real Postgres)
- Add post-deploy smoke tests for compliance and VCL endpoints
- Bump lint --max-warnings from 10 to 25
- Configure varsIgnorePattern for _ prefix convention on unused vars
Closes#19
The badge() style function used rgb-to-rgba string replacement for
the background, which doesn't work with hex colors. Hex colors passed
through unchanged as opaque backgrounds, hiding the text. Use hex
alpha notation (color + '26' = ~15% opacity) instead.
- 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
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.
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)
- 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).
- 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
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.
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.
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.