Sync .kiro/ from master — v2.2.0 release batch

New specs: archer-template-library, ccp-metrics-view-restructure,
compliance-list-stale-after-sidebar-edit, compliance-metric-estimated-resolution-date,
compliance-remediation-display-fix, flexible-jira-ticket-creation,
forecast-burndown-chart, granite-loader-export, ivanti-queue-clear-completed-fix,
multi-item-jira-ticket, queue-collapsible-sections, vendor-issue-type-dropdown

New steering: archer-template-gen.md

Updated: migration-registration-check hook, remediation-plan-history spec,
gitlab-workflow, tech, versioning steering files
This commit is contained in:
Jordan Ramos
2026-06-04 11:27:31 -06:00
parent 8ebd7e4d5e
commit a61d254ff9
54 changed files with 6992 additions and 59 deletions

View File

@@ -0,0 +1 @@
version = "1.0"

View File

@@ -0,0 +1,319 @@
# Design Document: Granite Loader Sheet Export
## Overview
This feature adds a Granite Team_Device Loader xlsx generator to the STEAM Security Dashboard. It integrates with the existing Ivanti Queue (for CARD/GRANITE items) and provides a standalone mode for ad-hoc device lists. The system enriches device data from the CARD API, allows bulk defaults with per-row overrides in an editable preview table, and generates a properly formatted xlsx for upload to SNIP XperLoad.
Key design decisions:
- **Frontend-driven xlsx generation**: The xlsx is generated client-side using the `xlsx` library (already a project dependency) to avoid backend file I/O and temp file cleanup.
- **Backend CARD enrichment endpoint**: A single batch endpoint accepts an array of IPs and returns enriched Granite fields from CARD. This keeps the frontend simple and centralizes CARD API token management.
- **Reuse queue data model**: Queue items already have `ip_address`, `hostname`, and `workflow_type` — no schema changes needed.
- **Modal-based UI**: The loader configuration lives in a modal (like the Consolidation Modal pattern) to avoid adding a new page while keeping the queue page clean.
## Architecture
```mermaid
flowchart TD
subgraph Frontend
QP[IvantiTodoQueuePage] -->|selected CARD/GRANITE items| LM[LoaderModal]
NAV[Nav Drawer / Standalone] -->|manual IP list| LM
LM -->|enrich request| API[POST /api/card/enrich-batch]
LM -->|generate xlsx| XLSX[xlsx library - client-side]
XLSX -->|download| FILE[Loader_Change_TEAM_DATE.xlsx]
end
subgraph Backend
API --> CARD[CARD API Helper]
CARD -->|per-IP lookup| EXT[card.charter.com]
end
```
## Components and Interfaces
### Backend: Batch Enrichment Endpoint
**File:** `backend/routes/cardApi.js` (added to existing router)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/card/enrich-batch` | Admin, Standard_User | Batch lookup IPs in CARD, return Granite-relevant fields |
**Request:**
```json
{
"ips": ["10.240.78.110", "10.240.78.111", "172.16.5.20"]
}
```
**Response:**
```json
{
"results": [
{
"ip": "10.240.78.110",
"found": true,
"equip_inst_id": "1931008",
"hostname": "NDCW-SW-CORE-01",
"site_name": "ENWDCOCD-PEAKVIEW-SRDC",
"mgmt_ip_asn": "11427",
"responsible_team": "NTS-AEO-STEAM",
"equipment_class": "S",
"equip_template": "DISC-CISCO NEXUS 9300",
"equip_status": "Active"
},
{
"ip": "172.16.5.20",
"found": false,
"equip_inst_id": null,
"hostname": null,
"error": "IP not found in CARD"
}
],
"enriched_count": 1,
"not_found_count": 1,
"total": 2
}
```
**Implementation notes:**
- Accepts up to 200 IPs per request (matches Requirement 1.4 limit).
- For each IP, constructs asset ID candidates with known suffixes (CTEC, NATL, CHTR, etc.) and queries `GET /api/v1/owner/{assetId}`.
- Falls back to team asset search if direct owner lookup fails.
- Extracts fields from `ncim_discovery`, `netops_granite_allips`, `card_flags`, and `owner` on the asset record.
- Returns partial results on CARD API errors (best-effort enrichment).
### Frontend: LoaderModal Component
**File:** `frontend/src/components/LoaderModal.js`
A modal component that handles the full loader sheet workflow:
**Props:**
```javascript
{
isOpen: boolean,
onClose: () => void,
// Pre-populated from queue selection (null in standalone mode)
initialDevices: Array<{ ip_address: string, hostname: string }> | null,
}
```
**Internal State:**
```javascript
{
operationType: 'Change' | 'Add' | 'Delete' | 'Move',
selectedColumns: Set<string>, // checked column IDs
devices: Array<DeviceRow>, // the editable row data
bulkDefaults: Record<string, string>, // column → default value
overrides: Record<string, Record<string, string>>, // rowIndex → column → value
enriching: boolean,
enrichErrors: Array<{ ip: string, error: string }>,
}
```
**DeviceRow shape:**
```javascript
{
ip_address: string,
hostname: string,
// CARD-enriched fields (populated after enrichment)
equip_inst_id: string | null,
site_name: string | null,
mgmt_ip_asn: string | null,
responsible_team: string | null,
equipment_class: string | null,
equip_template: string | null,
equip_status: string | null,
}
```
### Frontend: Column Configuration
**File:** `frontend/src/utils/graniteLoaderConfig.js`
Pure data module defining the 41 columns, their groupings, and operation-type requirements:
```javascript
export const LOADER_COLUMNS = [
{ id: 'DELETE', label: 'DELETE', group: 'Identification', requiredFor: ['Delete'] },
{ id: 'SET_CONFIRMED', label: 'SET_CONFIRMED', group: 'Identification', requiredFor: [] },
{ id: 'EQUIPMENT_CLASS', label: 'EQUIPMENT CLASS', group: 'Identification', requiredFor: ['Add'] },
{ id: 'EQUIP_INST_ID', label: 'EQUIP_INST_ID', group: 'Identification', requiredFor: ['Change', 'Move', 'Delete'] },
{ id: 'SITE_NAME', label: 'SITE_NAME', group: 'Identification', requiredFor: ['Add', 'Move'] },
{ id: 'EQUIP_NAME', label: 'EQUIP_NAME', group: 'Identification', requiredFor: ['Add'] },
{ id: 'EQUIP_TEMPLATE', label: 'EQUIP_TEMPLATE', group: 'Identification', requiredFor: ['Add'] },
{ id: 'EQUIP_STATUS', label: 'EQUIP_STATUS', group: 'Identification', requiredFor: ['Add'] },
{ id: 'RESPONSIBLE_TEAM', label: 'UDA#RESPONSIBLE ORGANIZATION#RESPONSIBLE TEAM', group: 'Responsible Org', requiredFor: ['Add'] },
{ id: 'IPV4_ADDRESS', label: 'UDA#IP_ADDRESSING#IPV4_ADDRESS', group: 'IP Addressing', requiredFor: ['Add'] },
{ id: 'MAC_ADDRESS', label: 'UDA#IP_ADDRESSING#MAC ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'MGMT_IP_ASN', label: 'UDA#IP_ADDRESSING#MGMT_IP_ASN', group: 'IP Addressing', requiredFor: ['Add'] },
{ id: 'SERIALNUMBER', label: 'SERIALNUMBER', group: 'Equipment Info', requiredFor: [] },
{ id: 'EXCLUDED_DISCOVERY', label: 'UDA#AUTO DISCOVERY#EXCLUDED FROM DISCOVERY', group: 'Discovery', requiredFor: [] },
{ id: 'EXCLUDED_REASON', label: 'UDA#AUTO DISCOVERY#EXCLUDED FROM DISCOVERY REASON', group: 'Discovery', requiredFor: [] },
{ id: 'IPV6_ADDRESS', label: 'UDA#IP_ADDRESSING#IPV6_ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'ILOM_ADDRESS', label: 'UDA#IP_ADDRESSING#IPV4_ILOM_ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'APP_ID_ASSET_TAG', label: 'UDA#CHERWELL_CMDB#APP ID ASSET TAG', group: 'Cyber Metrics', requiredFor: [] },
{ id: 'DEVICE_FUNCTION', label: 'UDA#CHERWELL_CMDB#DEVICE_FUNCTION', group: 'Cyber Metrics', requiredFor: [] },
{ id: 'ENVIRONMENT', label: 'UDA#CHERWELL_CMDB#ENVIRONMENT', group: 'Cyber Metrics', requiredFor: [] },
{ id: 'SECONDARY_MGMT_IP', label: 'UDA#IP_ADDRESSING#SECONDARY_MGMT_IP_ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'VIP', label: 'UDA#IP_ADDRESSING#VIP', group: 'IP Addressing', requiredFor: [] },
{ id: 'FLOATING_IP', label: 'UDA#IP_ADDRESSING#FLOATING IP ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'SCAN_IP_1', label: 'UDA#IP_ADDRESSING#SCAN IP ADDRESS 1', group: 'IP Addressing', requiredFor: [] },
{ id: 'SCAN_IP_2', label: 'UDA#IP_ADDRESSING#SCAN IP ADDRESS 2', group: 'IP Addressing', requiredFor: [] },
{ id: 'SCAN_IP_3', label: 'UDA#IP_ADDRESSING#SCAN IP ADDRESS 3', group: 'IP Addressing', requiredFor: [] },
{ id: 'EQUIP_MODEL', label: 'EQUIP_MODEL', group: 'Equipment Info', requiredFor: [] },
{ id: 'EQUIP_COMMENTS', label: 'EQUIP_COMMENTS', group: 'Equipment Info', requiredFor: [] },
{ id: 'EQUIP_PARTNUMBER', label: 'EQUIP_PARTNUMBER', group: 'Equipment Info', requiredFor: [] },
{ id: 'OS', label: 'UDA#CONTROLLER CONFIG#OS', group: 'Equipment Info', requiredFor: [] },
{ id: 'OS_VERSION', label: 'UDA#CONTROLLER CONFIG#OS VERSION', group: 'Equipment Info', requiredFor: [] },
{ id: 'CPU_CORES', label: 'UDA#CONTROLLER CONFIG#TOTAL CPU CORES', group: 'Equipment Info', requiredFor: [] },
{ id: 'RAM_GB', label: 'UDA#CONTROLLER CONFIG#RAM IN GB', group: 'Equipment Info', requiredFor: [] },
{ id: 'STORAGE_GB', label: 'UDA#CONTROLLER CONFIG#STORAGE IN GB', group: 'Equipment Info', requiredFor: [] },
{ id: 'ARCHER_ID', label: 'UDA#WIFI EQUIP INFO#ARCHER ID', group: 'Other', requiredFor: [] },
{ id: 'INSTALL_LOCATION', label: 'UDA#WIFI EQUIP INFO#INSTALL LOCATION', group: 'Other', requiredFor: [] },
{ id: 'SYSNAME', label: 'UDA#EQUIPMENT_INFO#SYSNAME', group: 'Other', requiredFor: [] },
{ id: 'LATITUDE', label: 'UDA#WIFI EQUIP INFO#LATITUDE', group: 'Other', requiredFor: [] },
{ id: 'LONGITUDE', label: 'UDA#WIFI EQUIP INFO#LONGITUDE', group: 'Other', requiredFor: [] },
{ id: 'OSTYPE', label: 'UDA#EQUIP MIGRATION#OSTYPE', group: 'Other', requiredFor: [] },
{ id: 'OSVERSION', label: 'UDA#EQUIP MIGRATION#OSVERSION', group: 'Other', requiredFor: [] },
];
export const COLUMN_GROUPS = [
'Identification',
'IP Addressing',
'Responsible Org',
'Discovery',
'Cyber Metrics',
'Equipment Info',
'Other',
];
export const OPERATION_TYPES = ['Change', 'Add', 'Delete', 'Move'];
```
### Frontend: XLSX Generation
**File:** `frontend/src/utils/graniteLoaderExport.js`
Pure function that takes the configured state and produces an xlsx workbook:
```javascript
/**
* @param {Object} config
* @param {string} config.operationType - 'Change' | 'Add' | 'Delete' | 'Move'
* @param {Array<string>} config.columnIds - ordered list of selected column IDs
* @param {Array<Object>} config.rows - device rows with resolved values (bulk + overrides merged)
* @returns {Blob} xlsx file as a Blob for download
*/
export function generateLoaderXlsx(config) { ... }
```
Uses the `xlsx` library (already in `frontend/package.json`) to create a workbook with a single "Load_Sheet" worksheet.
## Data Flow
### Queue-Initiated Flow
1. User selects CARD/GRANITE items on IvantiTodoQueuePage
2. User clicks "Generate Loader Sheet" in floating action bar
3. LoaderModal opens with `initialDevices` populated from selected items
4. User selects Operation Type (defaults to "Change")
5. User checks desired columns (required columns pre-checked)
6. User optionally clicks "Enrich from CARD" → `POST /api/card/enrich-batch`
7. EQUIP_INST_ID and other fields populate in the preview table
8. User sets bulk defaults and per-row overrides as needed
9. User clicks "Download" → client-side xlsx generation → browser download
### Standalone Flow
1. User navigates to standalone access point (nav drawer link or CARD page section)
2. LoaderModal opens with empty device list
3. User pastes IPs (textarea, one per line or comma-separated) → rows populate
4. Steps 49 same as above
### CARD Enrichment Flow (Backend)
1. Frontend sends `POST /api/card/enrich-batch` with array of IPs
2. Backend iterates IPs, for each:
a. Try `GET /api/v1/owner/{ip}-CTEC`, then `-NATL`, then `-CHTR` (known suffixes)
b. If found, extract fields from the asset record
c. If not found via owner lookup, search team assets for the IP
3. Return results array with found/not-found status per IP
## Error Handling
| Scenario | Behavior |
|----------|----------|
| CARD API not configured | "Enrich from CARD" button hidden; tooltip explains why |
| CARD API timeout on individual IP | Mark that IP as not-found, continue with others |
| CARD API auth failure | Show error toast, abort enrichment, preserve any already-enriched data |
| All IPs not found in CARD | Show warning banner "No devices found in CARD — enter EQUIP_INST_ID manually" |
| Required field missing on download | Highlight cells, show warning count, allow download with acknowledgment |
| xlsx generation failure | Show error toast with message |
| More than 200 IPs submitted | Frontend truncates to 200 with warning message |
## UI Layout
```
┌─────────────────────────────────────────────────────────────────┐
│ Generate Granite Loader Sheet [X] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Operation: [Change ▾] Devices: 24 items │
│ │
│ ┌─ Columns ──────────────────────────────────────────────────┐ │
│ │ ▸ Identification (4 selected) │ │
│ │ ▸ IP Addressing (2 selected) │ │
│ │ ▸ Responsible Org (1 selected) │ │
│ │ ▸ Cyber Metrics (0 selected) │ │
│ │ ▸ Equipment Info (0 selected) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ [Enrich from CARD] │
│ │
│ ┌─ Bulk Defaults ────────────────────────────────────────────┐ │
│ │ RESPONSIBLE TEAM: [NTS-AEO-STEAM ] │ │
│ │ EQUIP_STATUS: [Active ] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Preview (24 rows) ────────────────────────────────────────┐ │
│ │ IP Address │ EQUIP_INST_ID │ RESP TEAM │ STATUS │ │
│ │───────────────┼───────────────┼────────────────┼───────────│ │
│ │ 10.240.78.110 │ 1931008 │ NTS-AEO-STEAM │ Active │ │
│ │ 10.240.78.111 │ 1931009 │ NTS-AEO-STEAM │ Active │ │
│ │ 172.16.5.20 │ ⚠ (not found) │ ACCESS-ENG ● │ Active │ │
│ │ 172.16.5.21 │ 2045112 │ NTS-AEO-STEAM │ Active │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ⚠ 1 row missing EQUIP_INST_ID │
│ │
│ [Cancel] [Download Loader Sheet] │
└─────────────────────────────────────────────────────────────────┘
```
- ● = per-row override indicator (amber dot)
- ⚠ = missing required field or CARD lookup failure
## Testing Strategy
### Property-Based Tests
**File:** `backend/__tests__/granite-loader-enrichment.property.test.js`
- **Property 1: Enrichment result count** — For any array of N IPs (1 ≤ N ≤ 200), the response contains exactly N result objects.
- **Property 2: Found results have required fields** — For any result where `found === true`, `equip_inst_id` is a non-empty string.
- **Property 3: Not-found results have null fields** — For any result where `found === false`, `equip_inst_id` is null.
### Unit Tests
- Column configuration: required columns for each operation type
- XLSX generation: correct headers, correct row data, empty cells handled
- Bulk default + override merge logic
- IP validation
- EQUIP_INST_ID numeric validation
### Integration Tests
- `POST /api/card/enrich-batch` with mocked CARD API responses
- Auth requirement enforcement
- 200-IP limit enforcement

View File

@@ -0,0 +1,122 @@
# Requirements Document: Granite Loader Sheet Export
## Introduction
The STEAM Security Dashboard's Ivanti Queue already stages findings for CARD and GRANITE workflows. Once those items are worked, the next step is often submitting changes to Granite via the Team_Device Loader xlsx uploaded through SNIP XperLoad. Currently this requires manually downloading the 5MB template, finding EQUIP_INST_IDs via SNIP, and hand-filling rows — a tedious process for batches of 20+ devices.
This feature adds a "Generate Loader Sheet" action accessible from the Ivanti Queue that produces a properly formatted Team_Device Loader xlsx pre-populated with device data from the queue items, optionally enriched with EQUIP_INST_ID and other fields from the CARD API. Users select which columns they need, set bulk defaults with per-row overrides, and download a ready-to-upload xlsx.
## Glossary
- **Loader_Sheet**: The first worksheet ("Load_Sheet") in the Team_Device Loader xlsx workbook, containing the 41-column format that SNIP XperLoad accepts.
- **EQUIP_INST_ID**: The unique Granite identifier for an asset record. Required for Changes, Moves, and Deletes.
- **CARD_Enrichment**: The process of looking up an IP address in the CARD API to retrieve EQUIP_INST_ID, hostname, ASN, site, and other Granite-relevant fields.
- **Bulk_Default**: A value applied to all rows in a selected column. Can be overridden per-row.
- **Operation_Type**: One of Add, Change, Move, or Delete — determines which columns are required.
- **XperLoad**: The SNIP bulk loader tool that accepts the Team_Device Loader xlsx.
## Requirements
### Requirement 1: Generate Loader Sheet Action from Ivanti Queue
**User Story:** As a security analyst, I want to generate a Granite Loader Sheet directly from my CARD/GRANITE queue items, so that I don't have to manually look up device data and fill the template by hand.
#### Acceptance Criteria
1. WHEN the user has one or more CARD or GRANITE queue items selected (pending or completed), THE Queue page SHALL display a "Generate Loader Sheet" action button in the floating action bar.
2. WHEN the user clicks "Generate Loader Sheet", THE system SHALL open a Loader Configuration Modal pre-populated with the IP addresses and hostnames from the selected queue items.
3. THE action SHALL be available to users in the Admin and Standard_User groups.
4. THE action SHALL support any number of selected items from 1 to 200.
5. IF no CARD or GRANITE items are selected, THE "Generate Loader Sheet" button SHALL NOT appear.
### Requirement 2: Operation Type Selection
**User Story:** As a security analyst, I want to specify what type of Granite operation I'm performing, so that the generated sheet includes the correct required columns.
#### Acceptance Criteria
1. THE Loader Configuration Modal SHALL present an Operation Type selector with options: Change, Add, Delete, Move.
2. WHEN "Change" is selected, THE system SHALL require EQUIP_INST_ID (or SITE_NAME + EQUIP_NAME) as an identifier and allow any other column as an optional change field.
3. WHEN "Add" is selected, THE system SHALL require: EQUIPMENT CLASS, SITE_NAME, EQUIP_NAME, EQUIP_TEMPLATE, EQUIP_STATUS, UDA#RESPONSIBLE ORGANIZATION#RESPONSIBLE TEAM, UDA#IP_ADDRESSING#IPV4_ADDRESS, UDA#IP_ADDRESSING#MGMT_IP_ASN.
4. WHEN "Delete" is selected, THE system SHALL require DELETE column (auto-filled with "X") and EQUIP_INST_ID (or SITE_NAME + EQUIP_NAME).
5. WHEN "Move" is selected, THE system SHALL require EQUIP_INST_ID and SITE_NAME (the new site).
6. THE default Operation Type SHALL be "Change" since queue items are typically existing assets needing updates.
### Requirement 3: Column Selection
**User Story:** As a security analyst, I want to choose which columns to include in my loader sheet, so that I only fill in what's relevant to my specific task.
#### Acceptance Criteria
1. THE Loader Configuration Modal SHALL display a list of available columns as checkboxes, grouped by category (Identification, IP Addressing, Discovery, Responsible Org, Cyber Metrics, Equipment Info, Other).
2. Columns required by the selected Operation Type SHALL be pre-checked and non-dismissable.
3. THE user SHALL be able to check additional optional columns beyond the required set.
4. THE generated xlsx SHALL only include columns that are checked (required + user-selected optional).
5. THE column order in the generated xlsx SHALL match the canonical Team_Device Loader column order (DELETE first, then SET_CONFIRMED, EQUIPMENT CLASS, EQUIP_INST_ID, etc.).
### Requirement 4: Device List with Bulk Defaults and Per-Row Overrides
**User Story:** As a security analyst, I want to set a default value for a column that applies to all rows, but override it on specific rows when devices need different values.
#### Acceptance Criteria
1. THE Loader Configuration Modal SHALL display a preview table showing all devices (rows) and selected columns.
2. FOR each selected column, THE modal SHALL provide a "Bulk Default" input above the table that sets the value for all rows in that column.
3. WHEN a Bulk Default is set or changed, ALL rows in that column that have not been individually overridden SHALL update to the new default value.
4. THE user SHALL be able to click any cell in the preview table to edit its value inline, creating a per-row override.
5. Cells with per-row overrides SHALL display a visual indicator (amber dot) distinguishing them from bulk-defaulted cells.
6. THE user SHALL be able to clear a per-row override to revert a cell back to the bulk default.
7. THE preview table SHALL be scrollable for large device lists (20+ items) while keeping column headers and the bulk default row sticky.
### Requirement 5: CARD API Enrichment
**User Story:** As a security analyst, I want to automatically look up EQUIP_INST_ID and other Granite fields from the CARD API using the device IP, so that I don't have to manually search SNIP for each device.
#### Acceptance Criteria
1. THE Loader Configuration Modal SHALL include an "Enrich from CARD" toggle/button.
2. WHEN "Enrich from CARD" is activated, THE system SHALL look up each device's IP address via the CARD API and populate available fields: EQUIP_INST_ID, EQUIP_NAME (hostname), UDA#IP_ADDRESSING#MGMT_IP_ASN, SITE_NAME (if discoverable from CARD data).
3. THE enrichment SHALL display a progress indicator while lookups are in progress.
4. IF a device IP is not found in CARD, THE system SHALL leave the EQUIP_INST_ID cell empty and display a warning indicator on that row.
5. CARD-enriched values SHALL be treated as pre-populated defaults that can still be overridden per-row.
6. THE enrichment SHALL NOT overwrite values the user has already manually entered or overridden.
7. THE system SHALL handle CARD API errors gracefully — partial enrichment is acceptable (enrich what you can, warn on failures).
8. THE CARD enrichment endpoint SHALL require the CARD API to be configured (CARD_API_URL, CARD_API_USER, CARD_API_PASS set in .env). If not configured, the "Enrich from CARD" option SHALL be hidden.
### Requirement 6: XLSX Generation and Download
**User Story:** As a security analyst, I want to download a properly formatted xlsx file that I can upload directly to SNIP XperLoad without further editing.
#### Acceptance Criteria
1. WHEN the user clicks "Download", THE system SHALL generate an xlsx file with a single worksheet named "Load_Sheet".
2. THE first row SHALL contain column headers matching the exact canonical Team_Device Loader column names (e.g., "UDA#RESPONSIBLE ORGANIZATION#RESPONSIBLE TEAM", not abbreviated).
3. Each subsequent row SHALL contain the device data as configured in the preview table (bulk defaults + per-row overrides).
4. THE DELETE column SHALL be auto-populated with "X" for every row when Operation Type is "Delete".
5. THE EQUIPMENT CLASS column SHALL default to "S" (Shelf) unless overridden.
6. THE generated file SHALL be named with a descriptive pattern: `Loader_{operation}_{team}_{date}.xlsx` (e.g., `Loader_Change_NTS-AEO-STEAM_2026-05-27.xlsx`).
7. THE xlsx SHALL NOT include the reference data sheets (site names, templates, etc.) — only the Load_Sheet with data. This keeps the file small and focused.
8. Empty cells SHALL remain empty (not "null" or "undefined").
### Requirement 7: Standalone Access (Outside Queue Context)
**User Story:** As a security analyst, I want to access the Loader Sheet generator without going through the queue, for cases where I have a list of devices that aren't in my queue.
#### Acceptance Criteria
1. THE Loader Sheet generator SHALL be accessible as a standalone tool from the navigation drawer or a dedicated page section.
2. IN standalone mode, THE user SHALL be able to paste a list of IP addresses (one per line or comma-separated) to populate the device list.
3. IN standalone mode, THE user SHALL be able to manually add rows and fill in device identifiers (IP, hostname, or EQUIP_INST_ID).
4. ALL other functionality (operation type, column selection, bulk defaults, per-row overrides, CARD enrichment, xlsx download) SHALL work identically in standalone mode and queue-initiated mode.
### Requirement 8: Validation and Error Prevention
**User Story:** As a security analyst, I want the tool to warn me about missing required fields before I download, so that I don't upload an incomplete sheet to XperLoad.
#### Acceptance Criteria
1. BEFORE generating the xlsx, THE system SHALL validate that all required columns for the selected Operation Type have values in every row.
2. IF required fields are missing, THE system SHALL highlight the empty cells in red and display a summary warning (e.g., "3 rows missing EQUIP_INST_ID").
3. THE user SHALL be able to proceed with download despite warnings (XperLoad will reject invalid rows anyway), but must explicitly acknowledge the warning.
4. THE system SHALL validate IP address format (IPv4 pattern) for the IPV4_ADDRESS column if populated.
5. THE system SHALL validate EQUIP_INST_ID is numeric when populated.

View File

@@ -0,0 +1,151 @@
# Implementation Plan: Granite Loader Sheet Export
## Overview
Add a Granite Team_Device Loader xlsx generator accessible from the Ivanti Queue (for CARD/GRANITE items) and as a standalone tool. The system enriches device data from the CARD API, allows bulk defaults with per-row overrides in an editable preview table, and generates a properly formatted xlsx for upload to SNIP XperLoad. Implementation proceeds from column configuration utility → backend enrichment endpoint → frontend modal → queue integration → standalone access.
## Tasks
- [ ] 1. Create column configuration utility module
- [ ] 1.1 Create `frontend/src/utils/graniteLoaderConfig.js`
- Define `LOADER_COLUMNS` array with all 41 columns: id, label (exact Granite header name), group, requiredFor array
- Define `COLUMN_GROUPS` ordered array for UI grouping
- Define `OPERATION_TYPES` array: Change, Add, Delete, Move
- Export `getRequiredColumns(operationType)` helper that returns column IDs required for the given operation
- Export `getColumnsByGroup(group)` helper that returns columns in a group
- _Requirements: 2.12.6, 3.13.5_
- [ ] 1.2 Create `frontend/src/utils/graniteLoaderExport.js`
- Export `generateLoaderXlsx(config)` function that accepts `{ operationType, columnIds, rows }` and returns a Blob
- Use the `xlsx` library (already in frontend dependencies) to create a workbook with a single "Load_Sheet" worksheet
- First row contains exact canonical column headers from LOADER_COLUMNS (matched by columnIds, in canonical order)
- Subsequent rows contain device data; empty values become empty cells (not "null")
- DELETE column auto-filled with "X" when operationType is "Delete"
- EQUIPMENT CLASS defaults to "S" unless overridden
- Export `generateFilename(operationType, teamName)` helper returning `Loader_{op}_{team}_{YYYY-MM-DD}.xlsx`
- _Requirements: 6.16.8_
- [ ] 2. Backend CARD enrichment endpoint
- [ ] 2.1 Add `POST /api/card/enrich-batch` endpoint in `backend/routes/cardApi.js`
- Accept `{ ips: string[] }` in request body
- Validate: ips is a non-empty array, max 200 items, each item is a non-empty string
- Require Admin or Standard_User group
- Require CARD API to be configured (return 503 if not)
- For each IP, attempt owner lookup with known suffixes (CTEC, NATL, CHTR, etc.)
- Extract from asset record: equip_inst_id (from ncim_discovery, netops_granite_allips, or ise_granite_equipment), hostname, site_name, mgmt_ip_asn, responsible_team, equipment_class, equip_template, equip_status
- Return `{ results: [...], enriched_count, not_found_count, total }`
- Handle per-IP errors gracefully (mark as not-found, continue with others)
- Handle CARD API auth failures (return 502 with error message)
- _Requirements: 5.15.8_
- [ ] 2.2 Add `GET /api/card/configured` endpoint (or extend existing `/api/card/status`)
- Return `{ configured: boolean }` so the frontend knows whether to show the "Enrich from CARD" option
- This may already exist as `GET /api/card/status` — verify and reuse if so
- _Requirements: 5.8_
- [ ] 3. Frontend LoaderModal component
- [ ] 3.1 Create `frontend/src/components/LoaderModal.js`
- Accept props: `isOpen`, `onClose`, `initialDevices` (array of `{ ip_address, hostname }` or null)
- Render modal overlay with header "Generate Granite Loader Sheet"
- Include Operation Type selector (dropdown, defaults to "Change")
- Include Column Selection panel with collapsible groups and checkboxes
- Required columns for selected operation are pre-checked and disabled
- Include "Enrich from CARD" button (hidden if CARD not configured, checked via `/api/card/status` on mount)
- Include Bulk Defaults section: one input per selected column, setting value applies to all non-overridden rows
- Include editable Preview Table: rows = devices, columns = selected columns
- Cells are inline-editable on click; overridden cells show amber dot indicator
- Right-click or clear button on overridden cell reverts to bulk default
- Sticky column headers and bulk default row when scrolling
- Validation: highlight missing required fields in red, show warning count
- Download button: merges bulk defaults + overrides into final row data, calls `generateLoaderXlsx`, triggers browser download
- Cancel button closes modal
- _Requirements: 1.2, 2.12.6, 3.13.5, 4.14.7, 6.16.8, 8.18.5_
- [ ] 3.2 Implement CARD enrichment flow in LoaderModal
- On "Enrich from CARD" click, collect all device IPs, POST to `/api/card/enrich-batch`
- Show progress indicator during request
- On response, populate enriched fields into device rows (equip_inst_id, hostname, site_name, mgmt_ip_asn, etc.)
- Do NOT overwrite values the user has already manually entered
- Show warning indicators on rows where IP was not found
- Show error toast if CARD API auth fails
- _Requirements: 5.15.7_
- [ ] 3.3 Implement standalone mode (paste IPs)
- When `initialDevices` is null, show a textarea for pasting IPs (one per line or comma-separated)
- Parse input into device rows on "Load" button click
- Allow manually adding/removing rows via + and trash icons
- _Requirements: 7.17.4_
- [ ] 4. Integrate with Ivanti Queue page
- [ ] 4.1 Add "Generate Loader Sheet" button to IvantiTodoQueuePage floating action bar
- Show button when one or more selected items have workflow_type CARD or GRANITE
- Button label: "Generate Loader Sheet" with a FileSpreadsheet icon
- On click, open LoaderModal with `initialDevices` populated from selected items' ip_address and hostname
- _Requirements: 1.11.5_
- [ ] 4.2 Add standalone access point
- Add "Granite Loader" link in the navigation drawer under Tools section (or similar)
- Clicking opens LoaderModal in standalone mode (initialDevices = null)
- Alternatively, add a "Generate Loader Sheet" button on the CARD status section if one exists
- _Requirements: 7.1_
- [ ] 5. Checkpoint — Verify build and basic functionality
- Build frontend: `cd frontend && npm run build`
- Verify no lint errors or build failures
- Ensure all existing tests still pass
- Ask the user if questions arise
- [ ]* 6. Property-based tests for enrichment endpoint
- [ ]* 6.1 Write property test: Enrichment result count
- **Property 1: Result count equals input count** — For any array of N IPs (1 ≤ N ≤ 200), the response `results` array has exactly N elements
- File: `backend/__tests__/granite-loader-enrichment.property.test.js`
- **Validates: Requirements 5.1, 5.2**
- [ ]* 6.2 Write property test: Found results have equip_inst_id
- **Property 2: Found results have required fields** — For any result where `found === true`, `equip_inst_id` is a non-empty string
- **Validates: Requirements 5.2**
- [ ]* 6.3 Write property test: Not-found results have null fields
- **Property 3: Not-found results have null equip_inst_id** — For any result where `found === false`, `equip_inst_id` is null
- **Validates: Requirements 5.4**
- [ ]* 7. Unit tests for xlsx generation
- [ ]* 7.1 Write unit tests for `generateLoaderXlsx`
- Test correct column headers in canonical order
- Test DELETE column auto-fill for Delete operation
- Test EQUIPMENT CLASS defaults to "S"
- Test empty values produce empty cells (not "null" string)
- Test bulk default + override merge produces correct row values
- File: `backend/__tests__/granite-loader-xlsx-generation.test.js`
- **Validates: Requirements 6.16.8**
- [ ] 8. Final checkpoint
- Build frontend and verify no regressions
- Ensure all tests pass
- Ask the user if questions arise
## Notes
- Tasks marked with `*` are optional property-based and unit tests that can be skipped for faster MVP
- The `xlsx` library is already a frontend dependency — no new packages needed for xlsx generation
- The CARD API enrichment reuses the existing `cardApi.js` helper (token management, TLS skip, etc.)
- No database schema changes are required — this feature reads from queue items and CARD API only
- The LoaderModal follows the same pattern as ConsolidationModal (modal overlay, form state, action buttons)
- The preview table follows the same inline-edit pattern as the Reporting page (click to edit, amber dot for overrides)
- Maximum 200 devices per batch aligns with CARD API pagination limits and practical XperLoad batch sizes
## Task Dependency Graph
```json
{
"waves": [
{ "id": 0, "tasks": ["1.1", "1.2"] },
{ "id": 1, "tasks": ["2.1", "2.2"] },
{ "id": 2, "tasks": ["3.1"] },
{ "id": 3, "tasks": ["3.2", "3.3"] },
{ "id": 4, "tasks": ["4.1", "4.2"] },
{ "id": 5, "tasks": ["5"] },
{ "id": 6, "tasks": ["6.1", "6.2", "6.3", "7.1"] }
]
}
```