Add Archer Template Library for risk acceptance form reuse

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
This commit is contained in:
Jordan Ramos
2026-06-02 16:08:25 -06:00
parent c5225c96a5
commit 3500787851
11 changed files with 2648 additions and 133 deletions

View File

@@ -0,0 +1,427 @@
// ArcherTemplatePage.js
// Full-page Template Manager — browse, create, edit, clone, and delete
// Archer Risk Acceptance templates organized by Vendor > Platform > Model.
// Write operations require editor/admin role (Standard_User or Admin group).
import React, { useState, useEffect, useCallback } from 'react';
import {
FileText, Plus, Edit, Copy, Trash2, ChevronDown, ChevronRight,
Loader, AlertCircle, RefreshCw,
} from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import TemplateFormModal from '../TemplateFormModal';
import DeleteConfirmModal from '../DeleteConfirmModal';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
// ---------------------------------------------------------------------------
// Styles — dark theme tactical intelligence aesthetic
// ---------------------------------------------------------------------------
const STYLES = {
page: {
minHeight: '60vh',
},
card: {
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95), rgba(15, 23, 42, 0.98))',
border: '1px solid rgba(0, 212, 255, 0.15)',
borderRadius: '12px',
padding: '1.5rem',
marginBottom: '1rem',
},
header: {
fontFamily: 'monospace',
fontSize: '0.7rem',
fontWeight: 700,
color: '#00d4ff',
textTransform: 'uppercase',
letterSpacing: '0.15em',
marginBottom: '1rem',
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '1rem',
flexWrap: 'wrap',
gap: '0.5rem',
},
btn: {
padding: '0.5rem 1rem',
borderRadius: '8px',
border: '1px solid rgba(0, 212, 255, 0.3)',
background: 'rgba(0, 212, 255, 0.1)',
color: '#7DD3FC',
cursor: 'pointer',
fontSize: '0.8rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '0.4rem',
transition: 'all 0.2s',
},
btnDanger: {
padding: '0.4rem 0.75rem',
borderRadius: '6px',
border: '1px solid rgba(239, 68, 68, 0.3)',
background: 'rgba(239, 68, 68, 0.1)',
color: '#FCA5A5',
cursor: 'pointer',
fontSize: '0.75rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '0.3rem',
transition: 'all 0.2s',
},
btnSmall: {
padding: '0.35rem 0.65rem',
borderRadius: '6px',
border: '1px solid rgba(0, 212, 255, 0.25)',
background: 'rgba(0, 212, 255, 0.08)',
color: '#7DD3FC',
cursor: 'pointer',
fontSize: '0.75rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '0.3rem',
transition: 'all 0.2s',
},
vendorGroup: {
marginBottom: '0.75rem',
},
vendorHeader: {
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.6rem 0.75rem',
background: 'rgba(0, 212, 255, 0.05)',
border: '1px solid rgba(0, 212, 255, 0.12)',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.15s',
},
vendorLabel: {
fontSize: '0.85rem',
fontWeight: 700,
color: '#e0e0e0',
},
vendorCount: {
fontSize: '0.7rem',
color: '#64748B',
marginLeft: 'auto',
},
platformSubgroup: {
marginLeft: '1.25rem',
marginTop: '0.5rem',
paddingLeft: '0.75rem',
borderLeft: '2px solid rgba(0, 212, 255, 0.1)',
},
platformLabel: {
fontSize: '0.78rem',
fontWeight: 600,
color: '#94A3B8',
marginBottom: '0.35rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
},
templateRow: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0.45rem 0.6rem',
borderRadius: '6px',
marginBottom: '0.25rem',
transition: 'background 0.15s',
},
templateModel: {
fontSize: '0.82rem',
color: '#e0e0e0',
fontWeight: 500,
},
templateActions: {
display: 'flex',
alignItems: 'center',
gap: '0.35rem',
},
emptyState: {
textAlign: 'center',
padding: '3rem 1rem',
color: '#64748B',
fontSize: '0.9rem',
},
errorBanner: {
padding: '0.75rem 1rem',
borderRadius: '8px',
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.25)',
color: '#FCA5A5',
fontSize: '0.82rem',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
marginBottom: '1rem',
},
loadingState: {
textAlign: 'center',
padding: '3rem 1rem',
color: '#64748B',
fontSize: '0.85rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.75rem',
},
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Group templates by vendor, then by platform within each vendor.
* Returns: [ { vendor, platforms: [ { platform, templates: [...] } ] } ]
*/
function groupTemplates(templates) {
const vendorMap = {};
for (const t of templates) {
if (!vendorMap[t.vendor]) vendorMap[t.vendor] = {};
if (!vendorMap[t.vendor][t.platform]) vendorMap[t.vendor][t.platform] = [];
vendorMap[t.vendor][t.platform].push(t);
}
const vendors = Object.keys(vendorMap).sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: 'base' })
);
return vendors.map(vendor => {
const platforms = Object.keys(vendorMap[vendor]).sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: 'base' })
);
return {
vendor,
platforms: platforms.map(platform => ({
platform,
templates: vendorMap[vendor][platform].sort((a, b) =>
a.model.localeCompare(b.model, undefined, { sensitivity: 'base' })
),
})),
};
});
}
// ---------------------------------------------------------------------------
// ArcherTemplatePage
// ---------------------------------------------------------------------------
export default function ArcherTemplatePage() {
const { canWrite } = useAuth();
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expandedVendors, setExpandedVendors] = useState({});
// Modal state for create/edit/clone
const [modalState, setModalState] = useState({ open: false, mode: 'create', template: null });
const [deleteTarget, setDeleteTarget] = useState(null);
// -------------------------------------------------------------------------
// Fetch templates
// -------------------------------------------------------------------------
const fetchTemplates = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(`${API_BASE}/archer-templates`, {
credentials: 'include',
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Failed to fetch templates (${res.status})`);
}
const data = await res.json();
setTemplates(data);
// Expand all vendors by default on initial load
const expanded = {};
const grouped = groupTemplates(data);
for (const g of grouped) {
expanded[g.vendor] = true;
}
setExpandedVendors(expanded);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchTemplates();
}, [fetchTemplates]);
// -------------------------------------------------------------------------
// Vendor toggle
// -------------------------------------------------------------------------
const toggleVendor = (vendor) => {
setExpandedVendors(prev => ({ ...prev, [vendor]: !prev[vendor] }));
};
// -------------------------------------------------------------------------
// Grouped data
// -------------------------------------------------------------------------
const grouped = groupTemplates(templates);
const totalCount = templates.length;
// -------------------------------------------------------------------------
// Render
// -------------------------------------------------------------------------
return (
<div style={STYLES.page}>
<div style={STYLES.card}>
{/* Header */}
<div style={STYLES.header}>
<FileText size={12} style={{ display: 'inline', verticalAlign: 'middle', marginRight: '0.4rem' }} />
Archer Template Library
</div>
{/* Toolbar */}
<div style={STYLES.toolbar}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ color: '#94A3B8', fontSize: '0.8rem' }}>
{totalCount} template{totalCount !== 1 ? 's' : ''}
</span>
<button
style={STYLES.btn}
onClick={fetchTemplates}
title="Refresh"
>
<RefreshCw size={13} />
</button>
</div>
{canWrite() && (
<button
style={STYLES.btn}
onClick={() => setModalState({ open: true, mode: 'create', template: null })}
>
<Plus size={14} />
Create Template
</button>
)}
</div>
{/* Error banner */}
{error && (
<div style={STYLES.errorBanner}>
<AlertCircle size={14} />
{error}
</div>
)}
{/* Loading state */}
{loading && (
<div style={STYLES.loadingState}>
<Loader size={24} style={{ animation: 'spin 1s linear infinite' }} />
<span>Loading templates...</span>
</div>
)}
{/* Empty state */}
{!loading && !error && templates.length === 0 && (
<div style={STYLES.emptyState}>
<FileText size={32} style={{ marginBottom: '0.75rem', opacity: 0.4 }} />
<div>No templates found</div>
{canWrite() && (
<div style={{ marginTop: '0.5rem', fontSize: '0.8rem', color: '#475569' }}>
Click "Create Template" to add your first template.
</div>
)}
</div>
)}
{/* Template list grouped by vendor > platform */}
{!loading && grouped.map(({ vendor, platforms }) => (
<div key={vendor} style={STYLES.vendorGroup}>
{/* Vendor header — collapsible */}
<div
style={STYLES.vendorHeader}
onClick={() => toggleVendor(vendor)}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(0, 212, 255, 0.1)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(0, 212, 255, 0.05)'; }}
>
{expandedVendors[vendor]
? <ChevronDown size={14} style={{ color: '#00d4ff' }} />
: <ChevronRight size={14} style={{ color: '#64748B' }} />
}
<span style={STYLES.vendorLabel}>{vendor}</span>
<span style={STYLES.vendorCount}>
{platforms.reduce((sum, p) => sum + p.templates.length, 0)} template{platforms.reduce((sum, p) => sum + p.templates.length, 0) !== 1 ? 's' : ''}
</span>
</div>
{/* Platform subgroups */}
{expandedVendors[vendor] && platforms.map(({ platform, templates: platTemplates }) => (
<div key={platform} style={STYLES.platformSubgroup}>
<div style={STYLES.platformLabel}>{platform}</div>
{platTemplates.map(template => (
<div
key={template.id}
style={STYLES.templateRow}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(0, 212, 255, 0.04)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
>
<span style={STYLES.templateModel}>{template.model}</span>
{canWrite() && (
<div style={STYLES.templateActions}>
<button
style={STYLES.btnSmall}
onClick={() => setModalState({ open: true, mode: 'edit', template })}
title="Edit template"
>
<Edit size={12} />
</button>
<button
style={STYLES.btnSmall}
onClick={() => setModalState({ open: true, mode: 'clone', template })}
title="Clone template"
>
<Copy size={12} />
</button>
<button
style={STYLES.btnDanger}
onClick={() => setDeleteTarget(template)}
title="Delete template"
>
<Trash2 size={12} />
</button>
</div>
)}
</div>
))}
</div>
))}
</div>
))}
</div>
{/* Template form modal (create/edit/clone) */}
{modalState.open && (
<TemplateFormModal
mode={modalState.mode}
template={modalState.template}
onClose={() => setModalState({ open: false, mode: 'create', template: null })}
onSuccess={fetchTemplates}
/>
)}
{/* Delete confirmation modal */}
<DeleteConfirmModal
template={deleteTarget}
onConfirm={() => {
setDeleteTarget(null);
fetchTemplates();
}}
onCancel={() => setDeleteTarget(null)}
/>
</div>
);
}

View File

@@ -1,8 +1,9 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { ListTodo, RefreshCw, CheckSquare, Square, Loader, AlertCircle, X, Plus, CheckCircle, ChevronDown, ChevronRight, FileSpreadsheet } from 'lucide-react';
import { ListTodo, RefreshCw, CheckSquare, Square, Loader, AlertCircle, X, Plus, CheckCircle, ChevronDown, ChevronRight, FileSpreadsheet, FileText } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import ConsolidationModal from '../ConsolidationModal';
import LoaderModal from '../LoaderModal';
import TemplateSelector from '../TemplateSelector';
import { generateConsolidatedSummary, generateConsolidatedDescription, extractFirstCve, extractCommonVendor } from '../../utils/jiraConsolidation';
import { groupQueueItems } from '../../utils/queueGrouping';
import { VENDOR_PROJECT_KEYS, VENDOR_ISSUE_TYPES, STEAM_ISSUE_TYPES, isVendorProject, getIssueTypesForProject } from './JiraPage';
@@ -311,6 +312,9 @@ export default function IvantiTodoQueuePage() {
// Collapse state for grouped sections (Requirement 2.2, 2.7)
const [collapsedSections, setCollapsedSections] = useState({});
// Archer Template Selector panel — tracks which item ID has the panel expanded (Requirement 5.1)
const [templatePanelOpenId, setTemplatePanelOpenId] = useState(null);
// ---------------------------------------------------------------------------
// Data fetching
// ---------------------------------------------------------------------------
@@ -384,6 +388,13 @@ export default function IvantiTodoQueuePage() {
}));
}, []);
// ---------------------------------------------------------------------------
// Toggle Archer Template Selector panel (Requirement 5.1)
// ---------------------------------------------------------------------------
const toggleTemplatePanel = useCallback((itemId) => {
setTemplatePanelOpenId((prev) => (prev === itemId ? null : itemId));
}, []);
// ---------------------------------------------------------------------------
// Selection mode toggle (Requirement 1.1, 1.5)
// When deactivated, clear all selections
@@ -727,151 +738,202 @@ export default function IvantiTodoQueuePage() {
const cveDisplay = cves.length > 0
? cves.slice(0, 2).join(', ') + (cves.length > 2 ? ` +${cves.length - 2}` : '')
: '';
const isArcherItem = item.workflow_type === 'Archer';
const isTemplatePanelOpen = templatePanelOpenId === item.id;
return (
<div
key={item.id}
style={isSelected ? STYLES.queueItemSelected : STYLES.queueItem}
onClick={selectionMode ? () => toggleItemSelection(item.id) : undefined}
role={selectionMode ? 'button' : undefined}
tabIndex={selectionMode ? 0 : undefined}
onKeyDown={selectionMode ? (e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); toggleItemSelection(item.id); } } : undefined}
>
{/* Selection checkbox (Requirement 1.2) */}
{selectionMode && (
<input
type="checkbox"
checked={isSelected}
onChange={(e) => { e.stopPropagation(); toggleItemSelection(item.id); }}
onClick={(e) => e.stopPropagation()}
style={STYLES.checkbox}
aria-label={`Select ${item.finding_title || item.finding_id}`}
/>
)}
<React.Fragment key={item.id}>
<div
style={isSelected ? STYLES.queueItemSelected : STYLES.queueItem}
onClick={selectionMode ? () => toggleItemSelection(item.id) : undefined}
role={selectionMode ? 'button' : undefined}
tabIndex={selectionMode ? 0 : undefined}
onKeyDown={selectionMode ? (e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); toggleItemSelection(item.id); } } : undefined}
>
{/* Selection checkbox (Requirement 1.2) */}
{selectionMode && (
<input
type="checkbox"
checked={isSelected}
onChange={(e) => { e.stopPropagation(); toggleItemSelection(item.id); }}
onClick={(e) => e.stopPropagation()}
style={STYLES.checkbox}
aria-label={`Select ${item.finding_title || item.finding_id}`}
/>
)}
{/* Finding info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontFamily: 'monospace',
fontSize: '0.75rem',
fontWeight: 600,
color: '#CBD5E1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} title={item.finding_title || item.finding_id}>
{item.finding_title || item.finding_id}
</div>
{cveDisplay && (
{/* Finding info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontFamily: 'monospace',
fontSize: '0.65rem',
color: '#64748B',
marginTop: '2px',
fontSize: '0.75rem',
fontWeight: 600,
color: '#CBD5E1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} title={cves.join(', ')}>
{cveDisplay}
}} title={item.finding_title || item.finding_id}>
{item.finding_title || item.finding_id}
</div>
)}
</div>
{cveDisplay && (
<div style={{
fontFamily: 'monospace',
fontSize: '0.65rem',
color: '#64748B',
marginTop: '2px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} title={cves.join(', ')}>
{cveDisplay}
</div>
)}
</div>
{/* Ticket link badge (Requirements 6.3, 6.4) */}
{ticketLinks[item.id] && (
<a
href={ticketLinks[item.id].jira_url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
style={{
{/* Archer Template toggle button (Requirement 5.1) */}
{isArcherItem && (
<button
onClick={(e) => { e.stopPropagation(); toggleTemplatePanel(item.id); }}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.3rem',
padding: '0.2rem 0.5rem',
borderRadius: '4px',
border: isTemplatePanelOpen
? '1px solid rgba(0, 212, 255, 0.5)'
: '1px solid rgba(0, 212, 255, 0.2)',
background: isTemplatePanelOpen
? 'rgba(0, 212, 255, 0.15)'
: 'rgba(0, 212, 255, 0.05)',
color: isTemplatePanelOpen ? '#00d4ff' : '#7DD3FC',
cursor: 'pointer',
fontSize: '0.62rem',
fontFamily: 'monospace',
fontWeight: 600,
flexShrink: 0,
transition: 'all 0.2s',
}}
title={isTemplatePanelOpen ? 'Hide template selector' : 'Show template selector'}
aria-expanded={isTemplatePanelOpen}
aria-label="Toggle template selector"
>
<FileText style={{ width: '11px', height: '11px' }} />
{isTemplatePanelOpen ? 'Hide' : 'Template'}
</button>
)}
{/* Ticket link badge (Requirements 6.3, 6.4) */}
{ticketLinks[item.id] && (
<a
href={ticketLinks[item.id].jira_url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
style={{
fontFamily: 'monospace',
fontSize: '0.6rem',
fontWeight: 700,
color: '#6EE7B7',
background: 'rgba(16, 185, 129, 0.1)',
border: '1px solid rgba(16, 185, 129, 0.3)',
borderRadius: '999px',
padding: '0.15rem 0.5rem',
textDecoration: 'none',
whiteSpace: 'nowrap',
flexShrink: 0,
display: 'inline-flex',
alignItems: 'center',
gap: '0.25rem',
transition: 'all 0.2s',
}}
title={`Open ${ticketLinks[item.id].ticket_key} in Jira`}
>
{ticketLinks[item.id].ticket_key}
</a>
)}
{/* Workflow type badge */}
<div style={{
width: '80px',
textAlign: 'center',
flexShrink: 0,
}}>
<span style={{
fontFamily: 'monospace',
fontSize: '0.6rem',
fontWeight: 700,
color: '#6EE7B7',
background: 'rgba(16, 185, 129, 0.1)',
border: '1px solid rgba(16, 185, 129, 0.3)',
borderRadius: '999px',
padding: '0.15rem 0.5rem',
textDecoration: 'none',
whiteSpace: 'nowrap',
flexShrink: 0,
display: 'inline-flex',
alignItems: 'center',
gap: '0.25rem',
transition: 'all 0.2s',
}}
title={`Open ${ticketLinks[item.id].ticket_key} in Jira`}
>
{ticketLinks[item.id].ticket_key}
</a>
)}
{/* Workflow type badge */}
<div style={{
width: '80px',
textAlign: 'center',
flexShrink: 0,
}}>
<span style={{
fontFamily: 'monospace',
fontSize: '0.6rem',
fontWeight: 700,
color: wfColor.col,
background: `rgba(${wfColor.rgb}, 0.1)`,
border: `1px solid rgba(${wfColor.rgb}, 0.3)`,
borderRadius: '4px',
padding: '0.15rem 0.4rem',
textTransform: 'uppercase',
}}>
{item.workflow_type}
</span>
</div>
{/* Vendor */}
<div style={{
width: '120px',
flexShrink: 0,
fontFamily: 'monospace',
fontSize: '0.68rem',
color: '#94A3B8',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} title={item.vendor}>
{item.vendor || '—'}
</div>
{/* Hostname / IP */}
<div style={{
width: '120px',
flexShrink: 0,
minWidth: 0,
}}>
{item.hostname && (
<div style={{
fontFamily: 'monospace',
fontSize: '0.65rem',
color: '#94A3B8',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} title={item.hostname}>
{item.hostname}
</div>
)}
{item.ip_address && (
<div style={{
fontFamily: 'monospace',
fontSize: '0.62rem',
color: '#10B981',
marginTop: item.hostname ? '1px' : 0,
color: wfColor.col,
background: `rgba(${wfColor.rgb}, 0.1)`,
border: `1px solid rgba(${wfColor.rgb}, 0.3)`,
borderRadius: '4px',
padding: '0.15rem 0.4rem',
textTransform: 'uppercase',
}}>
{item.ip_address}
</div>
)}
{item.workflow_type}
</span>
</div>
{/* Vendor */}
<div style={{
width: '120px',
flexShrink: 0,
fontFamily: 'monospace',
fontSize: '0.68rem',
color: '#94A3B8',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} title={item.vendor}>
{item.vendor || '—'}
</div>
{/* Hostname / IP */}
<div style={{
width: '120px',
flexShrink: 0,
minWidth: 0,
}}>
{item.hostname && (
<div style={{
fontFamily: 'monospace',
fontSize: '0.65rem',
color: '#94A3B8',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} title={item.hostname}>
{item.hostname}
</div>
)}
{item.ip_address && (
<div style={{
fontFamily: 'monospace',
fontSize: '0.62rem',
color: '#10B981',
marginTop: item.hostname ? '1px' : 0,
}}>
{item.ip_address}
</div>
)}
</div>
</div>
</div>
{/* Archer Template Selector expandable panel (Requirement 5.1) */}
{isArcherItem && isTemplatePanelOpen && (
<div style={{
marginBottom: '0.5rem',
marginLeft: selectionMode ? '1.625rem' : '0',
padding: '0.75rem',
background: 'linear-gradient(135deg, rgba(22, 33, 62, 0.6), rgba(15, 23, 42, 0.8))',
border: '1px solid rgba(0, 212, 255, 0.15)',
borderTop: 'none',
borderRadius: '0 0 8px 8px',
}}>
<TemplateSelector />
</div>
)}
</React.Fragment>
);
})}
</div>