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
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.
Add ci.resolution_date and ci.remediation_plan to the GET /items endpoint
SELECT clause and update groupByHostname() to aggregate them as first-non-null
across each hostname's metric rows. The frontend already rendered these columns
but the list endpoint never fetched the data from the database.
Includes exploration and preservation property tests for groupByHostname().
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
The DELETE /completed endpoint failed with a FK violation when completed
queue items had associated rows in jira_ticket_queue_items. Replaced the
bare DELETE query with a transaction that removes junction table references
before deleting the queue items themselves.
Transaction sequence: BEGIN → SELECT completed IDs → DELETE junction rows →
DELETE queue items → COMMIT, with ROLLBACK on error and client release in
finally block.
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
GitLab CI runs each script line in a separate shell context, so sourcing
.env on one line doesn't carry to the next. Use export $(grep ...) on the
same line as jest to ensure DATABASE_URL is in the process environment.
Source DATABASE_URL from /home/cve-dashboard/backend/.env in test-backend job
so the integration test can connect to the local Postgres instance.
The test now skips gracefully via describe.skip when DATABASE_URL is unavailable
(defensive fallback), but with the env sourced it will run and validate migrations.
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.
The fileCreated hook was not reliably enforced. Switch to postToolUse
on write operations so the check fires inline immediately after any
file write, making it impossible to skip. The prompt self-filters to
only act when the written file is in backend/migrations/.
- 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.