diff --git a/RFCP-Iteration4-Critical-Fixes.md b/RFCP-Iteration4-Critical-Fixes.md
new file mode 100644
index 0000000..2be2ef2
--- /dev/null
+++ b/RFCP-Iteration4-Critical-Fixes.md
@@ -0,0 +1,419 @@
+# RFCP - Iteration 4: Critical Fixes
+
+## Issues Found in Production
+
+After Iteration 3 deployment, three critical issues identified:
+
+1. ❌ **Antenna directivity not working** - Coverage is omni even for sector antennas
+2. ❌ **Heatmap gradient broken** - Everything shows as solid red/orange
+3. ❌ **Terrain overlay invisible** - OpenTopoMap layer not visible enough
+
+---
+
+## CRITICAL FIX 1: Antenna Directivity
+
+**Problem:** Coverage calculation ignores antenna azimuth and beamwidth. A 75° sector antenna shows full 360° coverage.
+
+**Root Cause:** The coverage calculation worker doesn't filter points based on antenna direction.
+
+### Implementation
+
+**File:** `frontend/src/workers/coverage.worker.ts`
+
+Add directivity filter in the point loop:
+
+```typescript
+// In calculateCoverage function, after distance calculation:
+
+for (let latIdx = 0; latIdx < latPoints; latIdx++) {
+ for (let lonIdx = 0; lonIdx < lonPoints; lonIdx++) {
+ const lat = minLat + latIdx * latStep;
+ const lon = minLon + lonIdx * lonStep;
+
+ const distance = calculateDistance(site.lat, site.lon, lat, lon);
+
+ if (distance > radius) continue;
+
+ // NEW: Check antenna directivity
+ if (site.antennaType === 'sector') {
+ const bearing = calculateBearing(site.lat, site.lon, lat, lon);
+ const azimuth = site.azimuth || 0;
+ const beamwidth = 75; // degrees (could be configurable)
+
+ // Calculate angular difference
+ let angleDiff = Math.abs(bearing - azimuth);
+ if (angleDiff > 180) angleDiff = 360 - angleDiff;
+
+ // Skip points outside beamwidth
+ if (angleDiff > beamwidth / 2) continue;
+ }
+
+ // Calculate RSRP (existing code)
+ const fspl = calculateFSPL(distance, site.frequency);
+ const rsrp = site.power - fspl;
+
+ // ... rest of code
+ }
+}
+
+// Add helper function:
+function calculateBearing(lat1: number, lon1: number, lat2: number, lon2: number): number {
+ const φ1 = lat1 * Math.PI / 180;
+ const φ2 = lat2 * Math.PI / 180;
+ const Δλ = (lon2 - lon1) * Math.PI / 180;
+
+ const y = Math.sin(Δλ) * Math.cos(φ2);
+ const x = Math.cos(φ1) * Math.sin(φ2) -
+ Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
+
+ let θ = Math.atan2(y, x);
+ θ = θ * 180 / Math.PI; // radians to degrees
+
+ return (θ + 360) % 360; // normalize to 0-360
+}
+```
+
+### Visual Indicator
+
+**File:** `frontend/src/components/map/SiteMarkers.tsx`
+
+Add sector wedge visualization:
+
+```typescript
+import { Polygon } from 'react-leaflet';
+
+// In SiteMarker component, for sector antennas:
+{site.antennaType === 'sector' && (
+
+)}
+
+// Helper function:
+function generateSectorWedge(site: Site): [number, number][] {
+ const points: [number, number][] = [[site.lat, site.lon]];
+ const radius = 0.5; // km on map (visual only, not coverage radius)
+ const beamwidth = 75;
+ const azimuth = site.azimuth || 0;
+
+ const startAngle = azimuth - beamwidth / 2;
+ const endAngle = azimuth + beamwidth / 2;
+
+ // Generate arc points
+ for (let angle = startAngle; angle <= endAngle; angle += 5) {
+ const rad = angle * Math.PI / 180;
+ const latOffset = (radius / 111) * Math.cos(rad); // 1° lat ≈ 111km
+ const lonOffset = (radius / (111 * Math.cos(site.lat * Math.PI / 180))) * Math.sin(rad);
+
+ points.push([site.lat + latOffset, site.lon + lonOffset]);
+ }
+
+ points.push([site.lat, site.lon]); // close the wedge
+ return points;
+}
+```
+
+---
+
+## CRITICAL FIX 2: Heatmap Gradient (Retry)
+
+**Problem:** Heatmap shows solid red/orange at all zoom levels despite Iteration 3 fix attempt.
+
+**Root Cause:** The fix wasn't applied correctly, or RSRP values are outside expected range.
+
+### Debug First
+
+Check actual RSRP values being generated:
+
+```typescript
+// In Heatmap.tsx, add console.log:
+console.log('Heatmap Debug:', {
+ pointCount: points.length,
+ rsrpSample: points.slice(0, 5).map(p => p.rsrp),
+ rsrpMin: Math.min(...points.map(p => p.rsrp)),
+ rsrpMax: Math.max(...points.map(p => p.rsrp)),
+ mapZoom: mapZoom,
+ maxIntensity: maxIntensity
+});
+```
+
+### Apply Fix (Corrected)
+
+**File:** `frontend/src/components/map/Heatmap.tsx`
+
+```typescript
+export function Heatmap({ points, visible, opacity = 0.7 }: HeatmapProps) {
+ const map = useMap();
+ const [mapZoom, setMapZoom] = useState(map.getZoom());
+
+ useEffect(() => {
+ const handleZoomEnd = () => setMapZoom(map.getZoom());
+ map.on('zoomend', handleZoomEnd);
+ return () => { map.off('zoomend', handleZoomEnd); };
+ }, [map]);
+
+ if (!visible || points.length === 0) return null;
+
+ // CRITICAL: Normalize RSRP correctly
+ const normalizeRSRP = (rsrp: number): number => {
+ const minRSRP = -120; // Very weak signal
+ const maxRSRP = -70; // Excellent signal (CHANGED from -60)
+ const normalized = (rsrp - minRSRP) / (maxRSRP - minRSRP);
+ return Math.max(0, Math.min(1, normalized));
+ };
+
+ // Dynamic heatmap parameters
+ const radius = Math.max(8, Math.min(40, 50 - mapZoom * 2.5));
+ const blur = Math.max(6, Math.min(20, 30 - mapZoom * 1.5));
+
+ // CRITICAL: Dynamic max intensity to prevent saturation
+ const maxIntensity = Math.max(0.3, Math.min(1.0, 1.2 - mapZoom * 0.05));
+
+ // Convert to heatmap format
+ const heatmapPoints = points.map(p => [
+ p.lat,
+ p.lon,
+ normalizeRSRP(p.rsrp)
+ ] as [number, number, number]);
+
+ return (
+
+ p[1]}
+ latitudeExtractor={(p) => p[0]}
+ intensityExtractor={(p) => p[2]}
+ gradient={{
+ 0.0: '#0d47a1', // Dark Blue (very weak)
+ 0.2: '#00bcd4', // Cyan (weak)
+ 0.4: '#4caf50', // Green (fair)
+ 0.6: '#ffeb3b', // Yellow (good)
+ 0.8: '#ff9800', // Orange (strong)
+ 1.0: '#f44336', // Red (excellent)
+ }}
+ radius={radius}
+ blur={blur}
+ max={maxIntensity} // DYNAMIC!
+ minOpacity={0.3}
+ />
+
+ );
+}
+```
+
+### If Still Red After Fix
+
+The issue might be that ALL points have very strong RSRP (> -70 dBm). Solution:
+
+**Option A: Adjust normalization range**
+```typescript
+const minRSRP = -140; // Extend weak end
+const maxRSRP = -50; // Extend strong end
+```
+
+**Option B: Add RSRP clipping in worker**
+```typescript
+// In worker, clip RSRP to realistic range
+const rsrp = Math.max(-140, Math.min(-50, site.power - fspl));
+```
+
+---
+
+## CRITICAL FIX 3: Terrain Overlay Visibility
+
+**Problem:** OpenTopoMap terrain overlay is barely visible or completely invisible.
+
+**Root Causes:**
+1. Opacity too low
+2. Wrong layer order (under heatmap)
+3. Tile URL might be wrong
+
+### Fix Implementation
+
+**File:** `frontend/src/components/map/Map.tsx`
+
+```typescript
+import { TileLayer, LayersControl } from 'react-leaflet';
+
+// In Map component:
+
+ {/* Base map layer */}
+
+
+ {/* Terrain overlay - ABOVE base map, BELOW heatmap */}
+ {showTerrain && (
+
+ )}
+
+ {/* Heatmap on top */}
+
+
+ {/* Site markers on very top */}
+
+
+```
+
+### Alternative: Hillshade Layer
+
+If OpenTopoMap is too subtle, try dedicated hillshade:
+
+```typescript
+{showTerrain && (
+
+)}
+```
+
+### UI Improvements
+
+Add terrain opacity slider:
+
+**File:** `frontend/src/components/panels/CoverageSettings.tsx`
+
+```typescript
+ updateSettings({ terrainOpacity: value })}
+ suffix=""
+ help="Adjust terrain layer visibility"
+ disabled={!showTerrain}
+/>
+```
+
+---
+
+## TESTING CHECKLIST
+
+### Antenna Directivity Test:
+- [ ] Create sector antenna with azimuth 0° (north)
+- [ ] Coverage should show wedge facing north only
+- [ ] Create sector with azimuth 180° (south)
+- [ ] Coverage wedge should face south
+- [ ] Omni antenna should still show 360° coverage
+- [ ] Sector wedge visualization should match coverage area
+
+### Heatmap Gradient Test:
+- [ ] Zoom level 6-8: Smooth blue→red gradient
+- [ ] Zoom level 10-12: Colors distinct, no solid blob
+- [ ] Zoom level 14-16: Full gradient visible, especially blue/cyan at edges
+- [ ] Console log shows RSRP values in -120 to -70 range
+- [ ] maxIntensity value changes with zoom (check console)
+
+### Terrain Overlay Test:
+- [ ] Click Topo button: Contour lines visible
+- [ ] Terrain overlay shows hills/valleys clearly
+- [ ] Opacity slider adjusts terrain visibility
+- [ ] Terrain doesn't obscure heatmap or markers
+- [ ] Works in both light and dark theme
+
+---
+
+## BUILD & DEPLOY
+
+After changes:
+
+```bash
+cd /opt/rfcp/frontend
+
+# Build
+npm run build
+
+# Deploy
+sudo systemctl reload caddy
+
+# Test from VPS
+curl http://localhost:8888/health
+curl https://rfcp.eliah.one/ | head -20
+
+# Test from Windows
+# https://rfcp.eliah.one
+```
+
+---
+
+## COMMIT MESSAGE
+
+```
+fix(coverage): implement antenna directivity and sector patterns
+
+- Calculate bearing for each coverage point
+- Filter points outside sector beamwidth (75°)
+- Add sector wedge visualization on map
+- Omni antennas maintain 360° coverage
+
+fix(heatmap): resolve solid red gradient issue (retry)
+
+- Corrected RSRP normalization range (-120 to -70 dBm)
+- Applied dynamic max intensity based on zoom
+- Added debug logging for RSRP values
+- Full blue→red gradient now visible at all zoom levels
+
+fix(terrain): improve terrain overlay visibility
+
+- Increased opacity from 0.3 to 0.5
+- Correct layer ordering (base → terrain → heatmap)
+- Added terrain opacity slider
+- Alternative hillshade layer option
+```
+
+---
+
+## Expected Results
+
+**After Fix:**
+
+1. **Directivity:**
+ - Sector antenna (75°) shows wedge-shaped coverage
+ - Coverage concentrated in azimuth direction
+ - Visual sector indicator on map
+
+2. **Heatmap:**
+ - Blue at coverage edge (-120 dBm, weak)
+ - Cyan/green in medium range (-100 to -90 dBm)
+ - Yellow/orange closer to site (-85 to -75 dBm)
+ - Red only very close to site (-70 dBm, excellent)
+
+3. **Terrain:**
+ - Contour lines clearly visible
+ - Hills and valleys distinguishable
+ - Doesn't obscure coverage data
+ - Opacity adjustable
+
+---
+
+## Phase 4 Preview (Next)
+
+Once these fixes work, Phase 4 will add:
+
+- **Terrain Loss Calculation** - Height + elevation profile affects RSRP
+- **Backend Terrain API** - `/api/terrain/elevation?lat={}&lon={}`
+- **Line-of-Sight Analysis** - Check if path obstructed by terrain
+- **3D Terrain Visualization** - Optional 3D view with coverage overlay
+
+🚀 Good luck!