# 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.1–2.6, 3.1–3.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.1–6.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.1–5.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.1–2.6, 3.1–3.5, 4.1–4.7, 6.1–6.8, 8.1–8.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.1–5.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.1–7.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.1–1.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.1–6.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"] } ] } ```