diff --git a/RFCP-Iteration7-Elevation-UX.md b/RFCP-Iteration7-Elevation-UX.md
new file mode 100644
index 0000000..6fe231b
--- /dev/null
+++ b/RFCP-Iteration7-Elevation-UX.md
@@ -0,0 +1,569 @@
+# RFCP - Iteration 7: Elevation Data + UX Polish
+
+## Issues from Iteration 6
+
+1. ❌ **Omni circle visible** - orange circle shows for omni antennas, should be hidden
+2. ❌ **Zoom still breaks gradient** - colors shift with zoom (maxIntensity not working?)
+3. ❌ **No elevation visualization** - can't see hills/mountains on map
+4. ❌ **Sector cloning creates 3 sectors** - should clone 1 at a time
+
+---
+
+## QUICK FIX 1: Hide Omni Coverage Circle
+
+**Problem:** Orange circle shows for omni antennas (360° beamwidth).
+
+**Solution:** Only draw sector wedge for directional antennas (beamwidth < 360).
+
+**File:** `frontend/src/components/map/SiteMarker.tsx`
+
+```typescript
+// Only show wedge for directional sectors
+{site.sectors.map(sector => (
+ sector.enabled && sector.beamwidth < 360 && ( // ← ADD THIS CHECK
+
+ )
+))}
+```
+
+---
+
+## QUICK FIX 2: Debug Heatmap Zoom Issue
+
+**Problem:** Colors still change with zoom despite maxIntensity=0.75 fix.
+
+**Debug first:**
+
+**File:** `frontend/src/components/map/Heatmap.tsx`
+
+```typescript
+// Add detailed logging
+useEffect(() => {
+ if (import.meta.env.DEV && points.length > 0) {
+ const rsrpValues = points.map(p => p.rsrp);
+ const normalizedSample = points.slice(0, 5).map(p => ({
+ rsrp: p.rsrp,
+ normalized: normalizeRSRP(p.rsrp)
+ }));
+
+ console.log('🔍 Heatmap Debug:', {
+ zoom: mapZoom,
+ totalPoints: points.length,
+ rsrpRange: `${Math.min(...rsrpValues).toFixed(1)} to ${Math.max(...rsrpValues).toFixed(1)} dBm`,
+ radius,
+ blur,
+ maxIntensity, // ← Should be 0.75
+ sample: normalizedSample
+ });
+ }
+}, [points, mapZoom]);
+```
+
+**Possible issue:** If `maxIntensity` is still a formula instead of constant 0.75, replace it:
+
+```typescript
+// MUST be constant!
+const maxIntensity = 0.75; // NOT a formula!
+```
+
+---
+
+## QUICK FIX 3: Clone Single Sector
+
+**Problem:** Tri-sector preset creates 3 sectors at once. User wants to clone one sector at a time.
+
+**Solution:** Add "Clone Sector" button per sector.
+
+**File:** `frontend/src/components/panels/SectorConfig.tsx`
+
+```typescript
+const cloneSector = (sector: Sector) => {
+ const newSector: Sector = {
+ ...sector,
+ id: `s${Date.now()}`, // Unique ID
+ azimuth: (sector.azimuth + 30) % 360 // Offset by 30°
+ };
+ onUpdate([...sectors, newSector]);
+};
+
+// In sector item UI:
+
+
+ {sectors.length > 1 && (
+
+ )}
+
+```
+
+---
+
+## FEATURE 1: Elevation Visualization
+
+**What we want:**
+1. See elevation (meters above sea level) when hovering cursor
+2. Color-coded elevation overlay on map
+3. Elevation profile along measurement line
+
+### A. Cursor Elevation Display
+
+**Option 1: Use Open-Elevation API (free, no auth)**
+
+**File:** `frontend/src/hooks/useElevation.ts` (new)
+
+```typescript
+import { useState, useEffect } from 'react';
+import { useMap } from 'react-leaflet';
+import L from 'leaflet';
+
+export function useElevation() {
+ const map = useMap();
+ const [elevation, setElevation] = useState(null);
+ const [position, setPosition] = useState<{ lat: number; lon: number } | null>(null);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ let timeoutId: number;
+ let abortController: AbortController;
+
+ const handleMouseMove = (e: L.LeafletMouseEvent) => {
+ setPosition({ lat: e.latlng.lat, lon: e.latlng.lng });
+
+ // Debounce API calls (300ms)
+ clearTimeout(timeoutId);
+ if (abortController) abortController.abort();
+
+ timeoutId = window.setTimeout(async () => {
+ setLoading(true);
+ abortController = new AbortController();
+
+ try {
+ const response = await fetch(
+ `https://api.open-elevation.com/api/v1/lookup?locations=${e.latlng.lat},${e.latlng.lng}`,
+ { signal: abortController.signal }
+ );
+ const data = await response.json();
+ setElevation(data.results[0].elevation);
+ } catch (error) {
+ if (error.name !== 'AbortError') {
+ console.error('Elevation fetch failed:', error);
+ setElevation(null);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }, 300);
+ };
+
+ map.on('mousemove', handleMouseMove);
+
+ return () => {
+ map.off('mousemove', handleMouseMove);
+ clearTimeout(timeoutId);
+ if (abortController) abortController.abort();
+ };
+ }, [map]);
+
+ return { elevation, position, loading };
+}
+```
+
+**Display Component:**
+
+**File:** `frontend/src/components/map/ElevationDisplay.tsx` (new)
+
+```typescript
+import { useElevation } from '@/hooks/useElevation';
+
+export function ElevationDisplay() {
+ const { elevation, position, loading } = useElevation();
+
+ if (!position) return null;
+
+ return (
+
+
📍 {position.lat.toFixed(5)}°, {position.lon.toFixed(5)}°
+
+ ⛰️ {loading ? 'Loading...' : elevation !== null ? `${elevation}m ASL` : 'N/A'}
+
+
+ );
+}
+```
+
+**Usage in Map.tsx:**
+```typescript
+import { ElevationDisplay } from './ElevationDisplay';
+
+{showElevationInfo && }
+```
+
+### B. Elevation Color Overlay
+
+**Option: Use Stamen Terrain (color-coded by elevation)**
+
+**File:** `frontend/src/components/map/Map.tsx`
+
+```typescript
+{showElevationOverlay && (
+
+)}
+```
+
+**Add control:**
+```typescript
+
+```
+
+---
+
+## FEATURE 2: Site Templates & Quick Actions
+
+**What:** Preset site configurations for common deployments.
+
+**File:** `frontend/src/components/panels/SiteTemplates.tsx` (new)
+
+```typescript
+const SITE_TEMPLATES = {
+ urban_macro: {
+ name: 'Urban Macro Site',
+ height: 30,
+ power: 43,
+ frequency: 1800,
+ sectors: [
+ { azimuth: 0, beamwidth: 65, gain: 18 },
+ { azimuth: 120, beamwidth: 65, gain: 18 },
+ { azimuth: 240, beamwidth: 65, gain: 18 }
+ ]
+ },
+ rural_tower: {
+ name: 'Rural Tower',
+ height: 50,
+ power: 46,
+ frequency: 800,
+ sectors: [
+ { azimuth: 0, beamwidth: 360, gain: 8 } // Omni
+ ]
+ },
+ small_cell: {
+ name: 'Small Cell',
+ height: 6,
+ power: 30,
+ frequency: 2600,
+ sectors: [
+ { azimuth: 0, beamwidth: 90, gain: 12 }
+ ]
+ },
+ indoor_das: {
+ name: 'Indoor DAS',
+ height: 3,
+ power: 23,
+ frequency: 2100,
+ sectors: [
+ { azimuth: 0, beamwidth: 360, gain: 2 }
+ ]
+ }
+};
+
+export function SiteTemplates({ onApply }: { onApply: (template: any) => void }) {
+ return (
+
+
Quick Templates
+
+ {Object.entries(SITE_TEMPLATES).map(([key, template]) => (
+
+ ))}
+
+
+ );
+}
+```
+
+---
+
+## FEATURE 3: Coverage Analysis Tools
+
+### A. Best Server Map
+
+**What:** Show which site provides best signal at each point (Voronoi-like).
+
+**Implementation:** Color each point by `siteId` instead of RSRP.
+
+**File:** `frontend/src/store/coverage.ts`
+
+```typescript
+interface CoverageState {
+ // ...existing
+ viewMode: 'signal' | 'best-server'; // NEW
+}
+
+// In coverage calculation:
+if (viewMode === 'best-server') {
+ // Color by siteId instead of RSRP
+ return { lat, lon, siteId, rsrp };
+}
+```
+
+### B. Coverage Statistics
+
+**What:** Show coverage area, population, percentages.
+
+**File:** `frontend/src/components/panels/CoverageStats.tsx` (new)
+
+```typescript
+export function CoverageStats({ points, sites }: Props) {
+ const totalArea = calculateArea(points); // km²
+ const coverageByLevel = {
+ excellent: points.filter(p => p.rsrp > -70).length,
+ good: points.filter(p => p.rsrp > -85 && p.rsrp <= -70).length,
+ fair: points.filter(p => p.rsrp > -100 && p.rsrp <= -85).length,
+ weak: points.filter(p => p.rsrp <= -100).length
+ };
+
+ return (
+
+
Coverage Analysis
+
+
+ Total Coverage Area:
+ {totalArea.toFixed(1)} km²
+
+
+
+ Excellent (> -70 dBm):
+ {(coverageByLevel.excellent / points.length * 100).toFixed(1)}%
+
+
+
+ Good (-85 to -70 dBm):
+ {(coverageByLevel.good / points.length * 100).toFixed(1)}%
+
+
+
+ Fair (-100 to -85 dBm):
+ {(coverageByLevel.fair / points.length * 100).toFixed(1)}%
+
+
+
+ Weak (< -100 dBm):
+ {(coverageByLevel.weak / points.length * 100).toFixed(1)}%
+
+
+ );
+}
+```
+
+---
+
+## FEATURE 4: Import/Export Sites
+
+**What:** Save/load site configurations as JSON/CSV.
+
+**File:** `frontend/src/components/panels/SiteImportExport.tsx` (new)
+
+```typescript
+export function SiteImportExport() {
+ const { sites, setSites } = useSitesStore();
+
+ const exportSites = () => {
+ const json = JSON.stringify(sites, null, 2);
+ const blob = new Blob([json], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `rfcp-sites-${Date.now()}.json`;
+ a.click();
+ toast.success('Sites exported');
+ };
+
+ const importSites = async (file: File) => {
+ try {
+ const text = await file.text();
+ const imported = JSON.parse(text);
+ setSites(imported);
+ toast.success(`Imported ${imported.length} sites`);
+ } catch (error) {
+ toast.error('Invalid file format');
+ }
+ };
+
+ return (
+
+
Import/Export
+
+
+
+ e.target.files?.[0] && importSites(e.target.files[0])}
+ />
+
+ );
+}
+```
+
+---
+
+## FEATURE 5: 3D Terrain View (Future - Phase 4)
+
+**What:** Optional 3D view with MapLibre GL + terrain data.
+
+**Teaser for later:**
+- MapLibre GL for 3D rendering
+- Real terrain elevation from SRTM files
+- Line-of-sight visualization
+- 3D buildings in cities
+
+---
+
+## TESTING CHECKLIST (Iteration 7)
+
+### Quick Fixes:
+- [ ] Omni coverage circle hidden (only sectors show wedges)
+- [ ] Zoom gradient: Check console - maxIntensity=0.75 always?
+- [ ] Clone sector: Creates 1 sector (not 3)
+
+### Elevation:
+- [ ] Hover cursor shows elevation (meters ASL)
+- [ ] Elevation overlay shows color-coded terrain
+- [ ] Elevation display updates smoothly (debounced)
+
+### Templates:
+- [ ] Apply "Urban Macro" template → 3 sectors created
+- [ ] Apply "Rural Tower" → 1 omni sector
+- [ ] Apply "Small Cell" → appropriate settings
+
+### Coverage Stats:
+- [ ] Shows total coverage area in km²
+- [ ] Shows percentage by signal level
+- [ ] Updates after recalculation
+
+### Import/Export:
+- [ ] Export sites → downloads JSON file
+- [ ] Import JSON → restores sites correctly
+- [ ] Invalid file shows error
+
+---
+
+## BUILD & DEPLOY
+
+```bash
+cd /opt/rfcp/frontend
+npm run build
+sudo systemctl reload caddy
+```
+
+---
+
+## COMMIT MESSAGE
+
+```
+fix(ui): hide omni coverage circle visualization
+
+- Only show sector wedges for beamwidth < 360
+- Omni antennas (360°) no longer show orange circle
+
+fix(heatmap): debug zoom-dependent gradient issue
+
+- Added detailed console logging
+- Verify maxIntensity is constant 0.75
+
+feat(ux): clone single sector instead of tri-sector
+
+- Added "Clone Sector" button per sector
+- Creates duplicate with 30° azimuth offset
+- Removed automatic tri-sector creation
+
+feat(elevation): cursor elevation display
+
+- Shows elevation (meters ASL) at cursor position
+- Uses Open-Elevation API with debouncing
+- Optional elevation color overlay (Stamen Terrain)
+
+feat(templates): site configuration templates
+
+- Urban Macro (3-sector, 30m, 1800 MHz)
+- Rural Tower (omni, 50m, 800 MHz)
+- Small Cell (single sector, 6m, 2600 MHz)
+- Indoor DAS (omni, 3m, 2100 MHz)
+
+feat(analysis): coverage statistics panel
+
+- Total coverage area in km²
+- Signal quality breakdown (excellent/good/fair/weak)
+- Percentage distribution
+
+feat(io): import/export site configurations
+
+- Export sites as JSON
+- Import sites from JSON file
+- Preserves all site and sector settings
+```
+
+---
+
+## Iteration 7 Summary
+
+**Quick Fixes:**
+1. Hide omni circle
+2. Debug/fix zoom gradient
+3. Clone 1 sector (not 3)
+
+**New Features:**
+4. Elevation display (cursor + overlay)
+5. Site templates (4 presets)
+6. Coverage statistics
+7. Import/Export sites
+
+**Future (Iteration 8?):**
+- 3D terrain view (MapLibre GL)
+- Line-of-sight analysis
+- Population coverage estimation
+- Network capacity planning
+
+🚀 Ready for Iteration 7!