RESPONSIBLE_TEAM, EQUIP_STATUS, and EQUIPMENT_CLASS now show searchable
dropdown selectors in both the Bulk Defaults section and per-row inline
editing. Type to filter options, use arrow keys to navigate, Enter to select.
Picklist values extracted from docs/Team_Device Loader.xlsx reference sheets.
Per-row cells remain click-to-edit for all columns — picklist columns show
the SearchableSelect, free-text columns show a plain input.
When a CARD action fails with 'update_token not found', display a clear
message explaining the asset cannot be actioned via API, with a prominent
'Open in CARD (ID copied)' button that copies the host ID to clipboard and
opens card.charter.com/ipn-search in a new tab.
Applied to both CardDetailModal (reporting page) and CardActionModal (queue).
When the hostId fast path resolves via asset-search but the response lacks
an update_token, do a follow-up getOwner() call using the resolved _id to
fetch the token. Returns the rich owner data from asset-search merged with
the update_token from the owner endpoint.
The asset-search response wraps in { assets: [...] } and includes the full
owner record. Previously we tried to extract just an _id from the top level
(which didn't exist) and then made a separate getOwner() call that returned
empty data for IPv6 assets.
Now when hostId resolves via asset-search, we return the owner data directly
from the search response — no second API call needed. This fixes the tooltip
showing empty confirmed/unconfirmed for IPv6-only findings.
Findings with no IPv4 address now display Qualys IPv6 or Primary IPv6 as
fallback in the IP column, with a badge indicator:
- 'Q' (amber) = Qualys IPv6 from hostAdditionalDetails
- 'v6' (indigo) = Primary IPv6 from assetCustomAttributes
Priority: IPv4 > Qualys IPv6 > Primary IPv6
Backend changes:
- extractFinding now captures qualysIpv6 and primaryIpv6
- New extractQualysIpv6 helper parses hostAdditionalDetails
- upsertFindingsBatch stores both fields
- API response includes qualysIpv6 and primaryIpv6
- Migration adds qualys_ipv6 and primary_ipv6 columns
The Qualys IPv6 is preferred over Primary IPv6 because it resolves in CARD
(confirmed via testing with PMADEV-1).
The enrich-batch endpoint now accepts a host_ids array alongside ips.
When queue items have no IP address but have a host_id (from ivanti_findings),
the frontend sends host_ids and the backend resolves them via CARD asset-search.
Results include the resolved IP so it populates the IPV4_ADDRESS column.
The LoaderModal now carries _host_id from initialDevices through to the
enrich call.
The CARD asset-search endpoint returns the full enriched record (card_flags,
ivanti_assets, ncim_discovery, etc.) — same shape as team-assets. Before
falling back to the slow paginated team-assets loop, try each IP's host_id
via asset-search for direct single-call resolution.
Also registers the notifications table migration in run-all.js.
The migration file existed but was never registered in the POSTGRES_MIGRATIONS
array, so it never ran on production. The missing table caused 500 errors on
GET /api/notifications/count.
Integrate CARD's new v2 asset-search endpoint that accepts Ivanti Asset ID
integers directly, eliminating the slow suffix-guessing resolution flow.
Changes:
- Add searchByIvantiHostId() helper to cardApi.js
- Add GET /api/card/asset-search/:hostId endpoint
- Update CARD queue confirm/decline/redirect to try host_id fast path first
- Update owner-lookup to accept optional hostId query param for fast resolution
- Pass hostId through CardOwnerTooltip and ReportingPage for tooltip lookups
- Join ivanti_findings in todo queue GET to expose host_id on queue items
- Update CardActionModal to pass host_id for faster owner-lookup
openCreateJiraFromQueue only populated the description for Remediate
workflow items. Non-Remediate items got an empty string, while multi-select
worked because it used generateConsolidatedDescription via ConsolidationModal.
Now always includes finding info (vendor, title, CVEs, host/IP) in the
description for all workflow types. Remediate items still append their
notes below.
The older migration drops and re-adds the workflow_type CHECK constraint
but only included FP, Archer, CARD, GRANITE, DECOM. Once Remediate data
exists in the table, re-adding the old constraint fails. Added Remediate
to the constraint set so migrations can run idempotently in order.
- Single-item: openCreateJiraFromQueue fetches notes for Remediate items
and pre-fills the description with a Remediation Notes section
- Multi-item: ConsolidationModal fetches notes for all Remediate items
and appends them via appendRemediationNotes utility
Previously notes were only integrated in IvantiTodoQueuePage.js but
the actual Jira creation flow users interact with is in ReportingPage.js
QueuePanel and ConsolidationModal.
- Add remediationModalItem state to QueuePanel
- Render RemediationModal from QueuePanel for notes access
- Add Remediate color to wfColor mapping in renderQueueItem
- Add Remediate option to SelectionToolbar workflow type buttons
The Notes button and Remediate workflow option were only added to the
standalone IvantiTodoQueuePage but not the QueuePanel slide-out on the
Reporting Page, which is the primary interface for queue interaction.
RedirectModal had a hardcoded WORKFLOW_OPTIONS array that only included
FP, Archer, CARD, and GRANITE. Added DECOM and Remediate options, and
updated needsVendor check to require vendor for Remediate workflows.
- Add 'Remediate' as a valid workflow type (vendor-required, like FP/Archer)
- Create queue_remediation_notes table with FK cascade and 5000 char limit
- Add POST/GET /api/ivanti/todo-queue/:id/notes endpoints
- Include remediation_notes_count in queue item GET response
- Add RemediationModal component for viewing/adding notes
- Add notes count badge on Remediate queue items (purple #A855F7 theme)
- Add delete confirmation warning when removing items with notes
- Append remediation notes to Jira ticket descriptions
- Add property-based tests for all correctness properties
Adds a CategoryFilterBar with pill-shaped FilterChip components below the
metric health cards. Non-metric categories (Missing_AppID, Aging Vulns,
Missing_DF, etc.) are derived dynamically from device data and displayed
as color-coded filterable chips with device counts.
Unified filter state replaces the old metricFilter array, ensuring mutual
exclusivity between metric card filters and non-metric chip filters.
Includes 4 property-based tests validating derivation, filter predicate,
mutual exclusivity, and color resolution correctness.
Closes#26
Add per-metric stats and trend endpoints to vclMultiVertical.js. Refactor
CCPMetricsPage to use a unified MetricSelector that drives StatsBar, TrendChart,
DonutChart, and ForecastBurndownChart for the selected metric only. Remove the
separate Per-Metric Forecast Burndown section (now integrated). Fix trend query
double-counting when multiple uploads exist per vertical per month.
Closes#25
NoteCell now propagates saved notes back to the findings state via
onNoteSaved callback. This allows classifyFinding() to immediately
reclassify items from 'pending' to 'archer' when an EXC- note is
added, updating the Action Coverage donut without a page refresh.
Auto-populate description field when creating Jira tickets from the Archer
page with ticket metadata (EXC number, CVE, vendor, status, Archer URL).
Previously the description was always empty, requiring manual entry.
Includes security audit fixes for SQL injection prevention and input
validation in compliance, VCL multi-vertical, and CCP metrics routes.
Updates security audit tracker documentation.
Hover over any IP address in the findings table to see CARD ownership data
(confirmed/unconfirmed/candidate teams) in an interactive tooltip. Click
'Actions' to open a full modal for confirm/decline/redirect — no queue
item required.
Backend:
- Add direct /api/card/owner/:assetId/confirm|decline|redirect endpoints
- Add quick mode to resolveAssetId (CTEC only, 15s timeout) for tooltip use
- owner-lookup supports ?quick=1 query param with 504 on timeout
- getOwner accepts options for custom timeout
Frontend:
- New CardOwnerTooltip component (portal, hover bridge, cached results)
- New CardDetailModal for confirm/decline/redirect from tooltip
- IP cells show help cursor, trigger tooltip on 400ms hover
- Timeouts (504) not cached — retry on re-hover
- Teams fetch retries silently up to 3x on failure
- Redirect dropdowns show owner-data teams as fallback when teams API fails
Client-side grouping that collapses duplicate assets (same hostname + IP)
with multiple finding IDs into expandable host rows. Hosts with only one
finding remain as normal flat rows.
- Toggle button in toolbar switches between flat and grouped views
- Group header rows preserve column alignment (severity, host, IP in proper columns)
- Expanded sub-rows show full finding details with all interactions intact
- Selection, queue, hide, and workflow actions all work in both modes
- Groups sorted by highest severity; expand/collapse all controls included
Previously, redirecting a queue item required completing it first, which
created a duplicate entry. Now:
- Pending items: redirect updates workflow_type in place (no new row)
- Completed items: still creates a new pending item (legacy behavior)
- Redirect arrow now visible on all items, not just completed ones
- Frontend handles in-place updates by replacing the item in state
- Log the full owner response in audit when update_token is missing so
we can see what CARD actually returned
- Improve error message to suggest the asset may have already been actioned
- Remove backdrop-click-to-close on TemplateFormModal to prevent
accidental data loss while filling in template content
Adds a template management system to the Ivanti Queue's Archer Risk
Acceptance workflow. Templates store static form content (Environment
Overview, Segmentation, Mitigating Controls, etc.) organized by
Vendor > Platform > Model hierarchy.
Features:
- Full CRUD API at /api/archer-templates with search, filter, clone,
and hierarchy navigation endpoints
- Template Manager page (nav: Template Mgr) with grouped list view,
create/edit/clone/delete modals, role-based access
- TemplateSelector component integrated into Ivanti Todo Queue for
Archer workflow items with per-section copy-to-clipboard buttons
and Copy All functionality
- Database migration with case-insensitive uniqueness enforcement
- Audit logging for all template mutations
New files:
- backend/migrations/add_archer_templates_table.js
- backend/routes/archerTemplates.js
- frontend/src/components/pages/ArcherTemplatePage.js
- frontend/src/components/TemplateSelector.js
- frontend/src/components/TemplateFormModal.js
- frontend/src/components/DeleteConfirmModal.js
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
The enrich-batch handler was updated in df62e13 to search both
NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG across confirmed, unconfirmed, and
candidate dispositions, but the JSDoc comment still described the old
single-team, two-disposition behavior. Update the comment to match.
Search NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG across confirmed,
unconfirmed, and candidate dispositions. Assets that are only
candidates (not yet confirmed) were previously missed.
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.
The DNS ipv4first setting must be applied before any module loads
the https/http modules. When set inside cardApi.js helper, it's
too late — the https module has already cached DNS resolution
behavior. Moving it to the very top of server.js ensures it
takes effect globally for all outbound connections.
The family:4 option on individual requests wasn't sufficient.
Node.js 18 needs dns.setDefaultResultOrder('ipv4first') called
at module load time to prevent IPv6 resolution attempts to
card.charter.com which is unreachable via IPv6 from this network.
The owner endpoint only returns ownership info (no card_flags,
ncim_discovery, or netops_granite_allips). Switch to fetching
team assets (paginated) which returns the full enriched record
with EQUIP_INST_ID, CARD_HOSTNAME, CARD_ASN, CARD_DEVICE_ID,
CARD_VENDOR_MODEL, CARD_CLLI, and ncim_discovery data.
Accepts optional 'team' parameter (defaults to NTS-AEO-STEAM).
Paginates through confirmed and unconfirmed dispositions until
all target IPs are found or pages are exhausted.
The owner endpoint doesn't return ncim_discovery (so EQUIP_INST_ID
is unavailable from that endpoint). Update extractGraniteFields to
pull hostname from CARD_HOSTNAME, ASN from CARD_ASN, CLLI from
CARD_CLLI, serial from CARD_DEVICE_ID, and vendor/model from
CARD_VENDOR_MODEL in the card_flags array.
Assets without EQUIP_INST_ID are not in Granite and should use
the Add operation in the loader sheet instead of Change.
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 CARD API requires asset IDs in the format {IP}-{SUFFIX} (e.g.,
10.240.78.110-CTEC) but the frontend only has the bare IP. Add
resolveAssetId() helper that tries known suffixes (CTEC, NATL,
CHTR, COML, RESI, WIFI, VOIP) via owner lookup until one succeeds.
Apply resolution to confirm, decline, and redirect handlers so
they accept bare IPs from the frontend and resolve them
automatically before calling the CARD mutation APIs.
The /api/v1/teams endpoint returns 193 teams with nested objects
and can take longer than 15s to respond under load. Token
acquisition succeeds within 500ms but subsequent data calls
were hitting the 15s timeout.
card.charter.com resolves to both IPv4 (47.43.51.7) and IPv6
(2600:6c7f:9340:ca5::7). IPv6 is unreachable from this network,
causing Node.js to attempt IPv6 first, wait for timeout, then
fall back — but the 15s request timeout fires before the fallback
completes. Adding family: 4 to both acquireToken and doRequest
forces IPv4 resolution, matching curl behavior.
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