diff --git a/RFCP-Iteration10.3.1-Threshold-Filter-Fix.md b/RFCP-Iteration10.3.1-Threshold-Filter-Fix.md new file mode 100644 index 0000000..e2d7935 --- /dev/null +++ b/RFCP-Iteration10.3.1-Threshold-Filter-Fix.md @@ -0,0 +1,320 @@ +# RFCP - Iteration 10.3.1: Heatmap Threshold Filter Fix + +**Goal:** Fix purple "weak signal" still rendering by adding RSRP threshold filtering to heatmap renderer +**Priority:** P1 (Bug Fix) +**Estimated Time:** 15-20 minutes +**Status:** Ready for Implementation + +--- + +## πŸ“‹ Overview + +Iteration 10.3 added coverage boundary and changed default Min Signal to -100 dBm, but the heatmap tile renderer still draws all points including those below threshold. This creates the misleading purple fill that should have been removed. + +**Root Cause:** `HeatmapTileRenderer.ts` filters points only by geographic bounds, not by RSRP threshold. + +--- + +## πŸ› Current Bug + +**Symptom:** Purple/lavender fill visible outside the "useful" coverage zone despite Min Signal = -100 dBm + +**Location:** `src/components/map/HeatmapTileRenderer.ts` lines 100-107 + +**Current Code:** +```typescript +// Filter relevant points +const relevant = points.filter( + (p) => + p.lat >= latMin - bufferDeg && + p.lat <= latMax + bufferDeg && + p.lon >= lonMin - bufferDeg && + p.lon <= lonMax + bufferDeg +); +``` + +**Problem:** No RSRP threshold check β€” all points render regardless of signal strength. + +--- + +## βœ… Solution + +Add `rsrpThreshold` parameter to the rendering pipeline and filter out weak signals. + +### Step 1: Update HeatmapTileRenderer.ts + +**File:** `src/components/map/HeatmapTileRenderer.ts` + +#### 1.1 Add threshold to class state + +```typescript +// Around line 35, add new property: +export class HeatmapTileRenderer { + private tileSize = 256; + private radiusMeters: number; + private rsrpThreshold: number; // NEW + + // LRU cache... + + constructor(radiusMeters = 400, maxCacheSize = 150, rsrpThreshold = -100) { + this.radiusMeters = radiusMeters; + this.maxCacheSize = maxCacheSize; + this.rsrpThreshold = rsrpThreshold; // NEW + } +``` + +#### 1.2 Add setter for threshold + +```typescript +// After setRadiusMeters(), add: + +/** Update the RSRP threshold - points below this are not rendered. */ +setRsrpThreshold(threshold: number): void { + if (threshold !== this.rsrpThreshold) { + this.rsrpThreshold = threshold; + this.clearCache(); // Important: invalidate cache when threshold changes + } +} +``` + +#### 1.3 Update renderTile() filter + +```typescript +// Around line 100, update the filter: + +// Filter relevant points - geographic bounds AND RSRP threshold +const relevant = points.filter( + (p) => + p.rsrp >= this.rsrpThreshold && // πŸ”₯ NEW: skip weak signals + p.lat >= latMin - bufferDeg && + p.lat <= latMax + bufferDeg && + p.lon >= lonMin - bufferDeg && + p.lon <= lonMax + bufferDeg +); +``` + +### Step 2: Update GeographicHeatmap.tsx + +**File:** `src/components/map/GeographicHeatmap.tsx` + +#### 2.1 Add threshold prop + +```typescript +interface GeographicHeatmapProps { + points: HeatmapPoint[]; + visible: boolean; + opacity?: number; + radiusMeters?: number; + rsrpThreshold?: number; // NEW +} + +export default function GeographicHeatmap({ + points, + visible, + opacity = 0.7, + radiusMeters = 400, + rsrpThreshold = -100, // NEW +}: GeographicHeatmapProps) { +``` + +#### 2.2 Update renderer when threshold changes + +```typescript +// After the useEffect for radiusMeters, add: + +// Update renderer threshold when prop changes +useEffect(() => { + rendererRef.current.setRsrpThreshold(rsrpThreshold); +}, [rsrpThreshold]); +``` + +#### 2.3 Include threshold in cache invalidation + +```typescript +// Update the pointsHash useEffect to include threshold: + +useEffect(() => { + if (points.length === 0) { + rendererRef.current.setPointsHash('empty'); + return; + } + const first = points[0]; + const last = points[points.length - 1]; + // Include threshold in hash so cache invalidates when it changes + const hash = `${points.length}:${first.lat.toFixed(4)},${first.lon.toFixed(4)}:${last.rsrp}:${rsrpThreshold}`; + rendererRef.current.setPointsHash(hash); +}, [points, rsrpThreshold]); +``` + +### Step 3: Update App.tsx + +**File:** `src/App.tsx` + +Pass threshold from settings to GeographicHeatmap: + +```typescript +{coverageResult && ( + <> + + + +)} +``` + +--- + +## πŸ“ Files to Modify + +| File | Changes | +|------|---------| +| `src/components/map/HeatmapTileRenderer.ts` | Add `rsrpThreshold` property, setter, filter in renderTile() | +| `src/components/map/GeographicHeatmap.tsx` | Add `rsrpThreshold` prop, pass to renderer, update cache hash | +| `src/App.tsx` | Pass `settings.rsrpThreshold` to GeographicHeatmap | + +**Total:** 3 files, ~20 lines changed + +--- + +## πŸ§ͺ Testing Checklist + +### Visual Testing + +- [ ] **No purple fill:** Only orange gradient visible (no purple/lavender outside) +- [ ] **Threshold respected:** Changing Min Signal slider updates heatmap immediately +- [ ] **Boundary matches:** Dashed border aligns with heatmap edge +- [ ] **Different thresholds:** Test -90, -100, -110 β€” heatmap adjusts accordingly + +### Functional Testing + +- [ ] **Slider reactive:** Moving Min Signal slider updates heatmap without "Calculate Coverage" +- [ ] **Cache invalidation:** Threshold change clears old tiles, renders new ones +- [ ] **Zoom levels:** Works at zoom 8, 12, 16 +- [ ] **Multiple sites:** Each site respects threshold + +### Regression Testing + +- [ ] **50m resolution:** Still works without crash +- [ ] **Performance:** No significant slowdown +- [ ] **Boundary component:** Still renders correctly +- [ ] **Legend:** Still shows correct colors and threshold indicator + +--- + +## πŸ—οΈ Build & Deploy + +```bash +cd /opt/rfcp/frontend + +# Build +npm run build + +# Expected: 0 TypeScript errors, 0 ESLint errors + +# Deploy +sudo systemctl reload caddy + +# Test at https://rfcp.eliah.one +# 1. Set Min Signal to -100 dBm +# 2. Verify NO purple visible outside orange zone +# 3. Move slider to -90 β€” coverage zone shrinks +# 4. Move slider to -110 β€” coverage zone expands (some purple appears) +``` + +--- + +## πŸ“ Commit Message + +``` +fix(heatmap): filter points by RSRP threshold to remove purple fill + +- Added rsrpThreshold property to HeatmapTileRenderer +- Filter points below threshold before rendering tiles +- Cache invalidates when threshold changes +- Passed threshold from settings through GeographicHeatmap + +Points below Min Signal threshold are now completely hidden, +not just dimmed. This removes the misleading purple "weak signal" +fill that made it look like coverage existed where it doesn't. + +Iteration: 10.3.1 +Fixes: Purple fill still visible after 10.3 +``` + +--- + +## βœ… Success Criteria + +**Must Pass:** +1. βœ… No purple/lavender fill when Min Signal = -100 dBm +2. βœ… Heatmap updates when Min Signal slider moves +3. βœ… Only orange gradient visible (excellent β†’ fair signal) +4. βœ… Dashed boundary aligns with heatmap edge +5. βœ… TypeScript: 0 errors +6. βœ… ESLint: 0 errors + +**User Acceptance:** +- ОлСг confirms: "Ρ„Ρ–ΠΎΠ»Π΅Ρ‚Ρƒ Π½Π΅ΠΌΠ°, Ρ‚Ρ–Π»ΡŒΠΊΠΈ ΠΎΡ€Π°Π½ΠΆΠ΅Π²ΠΈΠΉ Π³Ρ€Π°Π΄Ρ–Ρ”Π½Ρ‚" + +--- + +## 🎨 Expected Visual Result + +**Before (10.3):** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β–“β–“β–“ Purple fill β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“ β”‚ ← Still visible! +β”‚ β–“β–“β”Œβ”€ ─ ─ ─ ─ ─ ─ ─ ─┐▓▓▓▓ β”‚ +β”‚ β–“β–“β”‚ Orange gradient β”‚β–“β–“β–“β–“ β”‚ +β”‚ ▓▓└─ ─ ─ ─ ─ ─ ─ ─ β”€β”˜β–“β–“β–“β–“ β”‚ +β”‚ β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**After (10.3.1):** +``` + β”Œβ”€ ─ ─ ─ ─ ─ ─┐ + β”‚ β”‚ + ─ ─ ── 🟠 Orange β”œβ”€ ─ ─ ← Dashed border + β”‚ gradient β”‚ + β”‚ (no purple β”‚ + β”‚ outside!) β”‚ + └─ ─ ─ ─ ─ ─ β”€β”˜ +``` + +--- + +## πŸ€– Instructions for Claude Code + +**Context:** +- Project: RFCP - `/opt/rfcp/frontend/` +- Bug: Purple fill still renders despite Min Signal = -100 dBm +- Root cause: `HeatmapTileRenderer.ts` doesn't filter by threshold + +**Implementation Order:** +1. `HeatmapTileRenderer.ts` β€” add threshold property + setter + filter +2. `GeographicHeatmap.tsx` β€” add prop, pass to renderer, update hash +3. `App.tsx` β€” pass `settings.rsrpThreshold` to component + +**Key Points:** +- Threshold setter MUST call `clearCache()` to invalidate tiles +- Include threshold in pointsHash for proper cache invalidation +- Default threshold = -100 dBm (matches store default) + +**Success:** User sees only orange gradient, no purple fill outside coverage zone + +--- + +**Document Created:** 2025-01-30 +**Author:** Claude (Opus 4.5) + ОлСг +**Status:** Ready for Implementation +**Depends On:** Iteration 10.3 (boundary component)