21 KiB
RFCP - Iteration 1: Fixes & Features
Comprehensive Task for Claude Code
Read RFCP-Fixes-Iteration1.md and RFCP-TechSpec-v3.0.md for context.
CRITICAL FIXES (Priority: HIGH)
1. Heatmap Color Gradient - More Obvious
Current: Green → Yellow → Red (not very distinct) New: Blue → Cyan → Green → Yellow → Orange → Red
File: frontend/src/components/map/Heatmap.tsx
// Update gradient in HeatmapLayer component:
gradient={{
0.0: '#0d47a1', // Dark Blue (very weak, -120 dBm)
0.2: '#00bcd4', // Cyan (weak, -110 dBm)
0.4: '#4caf50', // Green (fair, -100 dBm)
0.6: '#ffeb3b', // Yellow (good, -85 dBm)
0.8: '#ff9800', // Orange (strong, -70 dBm)
1.0: '#f44336', // Red (excellent, > -70 dBm)
}}
Reasoning:
- Cold colors (blue/cyan) = weak signal
- Warm colors (yellow/orange/red) = strong signal
- More intuitive for RF planning
- Better contrast and visibility
2. Coverage Radius - Increase to 100km
Current: Max 20km New: Max 100km with larger step
File: frontend/src/components/panels/SiteForm.tsx or wherever Coverage Settings panel is
// Find the radius slider and update:
<Slider
label="Radius"
min={1}
max={100} // ← Changed from 20
step={5} // ← Changed from 1 (easier control)
value={coverageSettings.radius}
onChange={(value) => updateCoverageSettings({ radius: value })}
suffix="km"
help="Calculation area around each site"
/>
Also update default value:
File: frontend/src/store/coverage.ts (or wherever initial settings are)
const defaultSettings: CoverageSettings = {
radius: 10, // Changed from 20 to 10 (better default)
resolution: 200, // 200m
rsrpThreshold: -120
};
3. "Save & Calculate" Button - Primary Action
Current: Separate "Save" and "Calculate" buttons New: "Save & Calculate" as primary action + "Save Only" as secondary
File: frontend/src/components/panels/SiteForm.tsx
// Replace button section:
<div className="flex gap-2">
<Button
onClick={handleSaveAndCalculate}
variant="primary"
className="flex-1"
>
💾 Save & Calculate
</Button>
<Button
onClick={handleSave}
variant="secondary"
>
💾 Save Only
</Button>
<Button
onClick={handleDelete}
variant="danger"
>
🗑️
</Button>
</div>
// Add handler function:
const handleSaveAndCalculate = async () => {
try {
// Save the site first
await handleSave();
// Get all sites from store
const sites = useSitesStore.getState().sites;
if (sites.length === 0) {
toast.info('Add at least one site to calculate coverage');
return;
}
// Trigger coverage calculation
const coverageStore = useCoverageStore.getState();
await coverageStore.calculateCoverage(sites);
// Success feedback
toast.success(`Coverage calculated for ${sites.length} site(s)!`);
} catch (error) {
toast.error(`Failed: ${error.message}`);
}
};
4. Legend Colors - Match New Gradient
File: frontend/src/components/map/Legend.tsx
const signalRanges = [
{
label: 'Excellent',
range: '> -70 dBm',
color: '#f44336', // Red
description: 'Very strong signal'
},
{
label: 'Good',
range: '-70 to -85 dBm',
color: '#ff9800', // Orange
description: 'Strong signal'
},
{
label: 'Fair',
range: '-85 to -100 dBm',
color: '#ffeb3b', // Yellow
description: 'Acceptable signal'
},
{
label: 'Poor',
range: '-100 to -110 dBm',
color: '#4caf50', // Green
description: 'Weak signal'
},
{
label: 'Weak',
range: '-110 to -120 dBm',
color: '#00bcd4', // Cyan
description: 'Very weak signal'
},
{
label: 'No Service',
range: '< -120 dBm',
color: '#0d47a1', // Dark Blue
description: 'No coverage'
},
];
Also update constants:
File: frontend/src/constants/rsrp-thresholds.ts
export const SIGNAL_COLORS = {
excellent: '#f44336', // Red
good: '#ff9800', // Orange
fair: '#ffeb3b', // Yellow
poor: '#4caf50', // Green
weak: '#00bcd4', // Cyan
'no-service': '#0d47a1' // Dark Blue
} as const;
UI IMPROVEMENTS (Priority: HIGH)
5. Dark Theme Support 🌙
Add complete dark mode with toggle
Files to create/update:
frontend/src/store/settings.ts- theme statefrontend/src/components/ui/ThemeToggle.tsx- toggle buttonfrontend/tailwind.config.js- dark mode configfrontend/src/index.css- dark theme styles
Implementation:
A) Tailwind Config:
// tailwind.config.js
export default {
darkMode: 'class', // Enable class-based dark mode
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
// Custom colors for dark mode
dark: {
bg: '#1a1a1a',
surface: '#2d2d2d',
border: '#404040',
text: '#e0e0e0',
}
}
},
},
plugins: [],
}
B) Settings Store:
// src/store/settings.ts (create if doesn't exist)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface SettingsState {
theme: 'light' | 'dark' | 'system';
setTheme: (theme: 'light' | 'dark' | 'system') => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'system',
setTheme: (theme) => {
set({ theme });
applyTheme(theme);
},
}),
{
name: 'rfcp-settings',
}
)
);
function applyTheme(theme: 'light' | 'dark' | 'system') {
const root = window.document.documentElement;
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
root.classList.toggle('dark', systemTheme === 'dark');
} else {
root.classList.toggle('dark', theme === 'dark');
}
}
// Apply theme on page load
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('rfcp-settings');
if (stored) {
const { theme } = JSON.parse(stored);
applyTheme(theme);
}
}
C) Theme Toggle Component:
// src/components/ui/ThemeToggle.tsx
import { useSettingsStore } from '@/store/settings';
export function ThemeToggle() {
const { theme, setTheme } = useSettingsStore();
const icons = {
light: '☀️',
dark: '🌙',
system: '💻'
};
return (
<div className="flex items-center gap-1 bg-gray-100 dark:bg-dark-surface rounded-lg p-1">
{(['light', 'dark', 'system'] as const).map((t) => (
<button
key={t}
onClick={() => setTheme(t)}
className={`
px-3 py-1.5 rounded text-sm transition-all
${theme === t
? 'bg-white dark:bg-dark-bg shadow-sm'
: 'hover:bg-gray-200 dark:hover:bg-dark-border'
}
`}
title={t.charAt(0).toUpperCase() + t.slice(1)}
>
{icons[t]}
</button>
))}
</div>
);
}
D) Update App.tsx:
// Add ThemeToggle to header
import { ThemeToggle } from '@/components/ui/ThemeToggle';
function App() {
return (
<div className="h-screen flex flex-col bg-white dark:bg-dark-bg">
<header className="bg-slate-800 dark:bg-slate-900 text-white px-4 py-3 flex items-center justify-between">
<h1 className="text-xl font-bold">RFCP - RF Coverage Planner</h1>
<ThemeToggle />
</header>
{/* rest of app */}
</div>
);
}
E) Dark Mode Styles for Components:
Update all major components with dark mode classes:
// Example pattern:
<div className="bg-white dark:bg-dark-surface text-gray-900 dark:text-dark-text">
{/* content */}
</div>
// Borders:
className="border border-gray-200 dark:border-dark-border"
// Inputs:
className="bg-white dark:bg-dark-bg border-gray-300 dark:border-dark-border"
// Buttons:
className="bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
Apply dark mode to:
- Map panels (SiteForm, SiteList, Coverage Settings)
- Legend component
- Toast notifications
- Modal overlays
- Input fields and sliders
6. Sites List - Show More Info
Current: Just site name New: Show key parameters inline
File: frontend/src/components/panels/SiteList.tsx
// Update site item rendering:
<div className="site-item p-3 border-b hover:bg-gray-50 dark:hover:bg-dark-surface">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* Color indicator */}
<div
className="w-4 h-4 rounded-full border-2 border-white shadow"
style={{ backgroundColor: site.color }}
/>
{/* Site info */}
<div>
<div className="font-medium text-gray-900 dark:text-dark-text">
{site.name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
📻 {site.frequency} MHz • ⚡ {site.power} dBm •
📡 {site.antennaType === 'omni' ? 'Omni' : `Sector ${site.azimuth}°`}
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-1">
<Button onClick={() => onEdit(site.id)} size="sm" variant="ghost">
Edit
</Button>
<Button onClick={() => onDelete(site.id)} size="sm" variant="ghost">
×
</Button>
</div>
</div>
</div>
7. Keyboard Shortcuts
Add useful shortcuts for power users
File: frontend/src/App.tsx or create src/hooks/useKeyboardShortcuts.ts
// Create hook:
// src/hooks/useKeyboardShortcuts.ts
import { useEffect } from 'react';
import { useSitesStore } from '@/store/sites';
import { useCoverageStore } from '@/store/coverage';
import { toast } from '@/components/ui/Toast';
export function useKeyboardShortcuts() {
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
// Ignore if typing in input
if (e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement) {
return;
}
const isMac = navigator.platform.toLowerCase().includes('mac');
const modKey = isMac ? e.metaKey : e.ctrlKey;
if (modKey) {
switch(e.key.toLowerCase()) {
case 's': // Ctrl/Cmd+S: Save current site
e.preventDefault();
const selectedSite = useSitesStore.getState().selectedSite;
if (selectedSite) {
useSitesStore.getState().updateSite(selectedSite.id, selectedSite);
toast.success('Site saved');
}
break;
case 'enter': // Ctrl/Cmd+Enter: Calculate coverage
e.preventDefault();
const sites = useSitesStore.getState().sites;
if (sites.length > 0) {
useCoverageStore.getState().calculateCoverage(sites);
} else {
toast.info('Add sites first');
}
break;
case 'n': // Ctrl/Cmd+N: New site (enter placement mode)
e.preventDefault();
useSitesStore.getState().setPlacementMode(true);
toast.info('Click on map to place new site');
break;
}
}
// Non-modifier shortcuts
switch(e.key) {
case 'Escape': // Escape: Cancel/close
useSitesStore.getState().setSelectedSite(null);
useSitesStore.getState().setPlacementMode(false);
break;
case 'h': // H: Toggle heatmap
useCoverageStore.getState().toggleHeatmap();
break;
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);
}
// Use in App.tsx:
function App() {
useKeyboardShortcuts();
// ...
}
Add keyboard shortcuts help:
// Add to header or settings:
<button
onClick={() => setShowShortcuts(true)}
className="text-sm text-gray-500 hover:text-gray-700"
title="Keyboard shortcuts"
>
⌨️
</button>
{showShortcuts && (
<div className="modal">
<h3>Keyboard Shortcuts</h3>
<ul>
<li><kbd>Ctrl/Cmd</kbd> + <kbd>S</kbd> - Save site</li>
<li><kbd>Ctrl/Cmd</kbd> + <kbd>Enter</kbd> - Calculate coverage</li>
<li><kbd>Ctrl/Cmd</kbd> + <kbd>N</kbd> - New site</li>
<li><kbd>Esc</kbd> - Cancel/Close</li>
<li><kbd>H</kbd> - Toggle heatmap</li>
</ul>
</div>
)}
8. Map Controls - Fit to Sites & Reset View
Add helpful map navigation buttons
File: frontend/src/components/map/Map.tsx
// Add control buttons:
<div className="absolute top-4 right-4 z-[1000] flex flex-col gap-2">
{/* Fit to sites */}
<button
onClick={handleFitToSites}
className="
bg-white dark:bg-dark-surface shadow-lg rounded px-3 py-2
hover:bg-gray-50 dark:hover:bg-dark-border
transition-colors
"
title="Fit view to all sites"
disabled={sites.length === 0}
>
🎯 Fit
</button>
{/* Reset to Ukraine */}
<button
onClick={handleResetView}
className="
bg-white dark:bg-dark-surface shadow-lg rounded px-3 py-2
hover:bg-gray-50 dark:hover:bg-dark-border
transition-colors
"
title="Reset to Ukraine view"
>
🏠 Reset
</button>
</div>
// Handlers:
const handleFitToSites = () => {
if (sites.length === 0) return;
const bounds = sites.map(site => [site.lat, site.lon] as [number, number]);
mapRef.current?.fitBounds(bounds, { padding: [50, 50] });
};
const handleResetView = () => {
mapRef.current?.setView([48.4, 35.0], 6); // Ukraine center
};
9. Better Error Handling
Add comprehensive error handling with helpful messages
File: frontend/src/store/coverage.ts
calculateCoverage: async (sites: Site[]) => {
if (sites.length === 0) {
toast.error('No sites to calculate');
return;
}
set({ isCalculating: true, error: null });
try {
const settings = get().settings;
// Validation
if (settings.radius > 100) {
throw new Error('Radius too large (max 100km)');
}
if (settings.resolution < 50) {
throw new Error('Resolution too fine (min 50m)');
}
// Calculate
const result = await calculator.calculateCoverage(sites, bounds, settings);
if (result.points.length === 0) {
toast.warning('No coverage points found. Try increasing radius or lowering threshold.');
} else {
toast.success(`Calculated ${result.points.length.toLocaleString()} points in ${(result.calculationTime / 1000).toFixed(1)}s`);
}
set({
coveragePoints: result.points,
isCalculating: false
});
} catch (error) {
console.error('Coverage calculation error:', error);
// User-friendly error messages
let message = 'Calculation failed';
if (error.message.includes('timeout')) {
message = 'Calculation timeout. Try reducing radius or increasing resolution.';
} else if (error.message.includes('worker')) {
message = 'Web Worker error. Please refresh the page.';
} else if (error.message.includes('memory')) {
message = 'Out of memory. Try smaller radius or coarser resolution.';
}
toast.error(message);
set({
error: error.message,
isCalculating: false
});
}
}
10. Mobile Responsiveness Improvements
Ensure panels don't block map on mobile
Files: SiteForm.tsx, SiteList.tsx, Legend.tsx
// Pattern for mobile-friendly panels:
<div className="
fixed bottom-0 left-0 right-0 // Mobile: bottom sheet
md:static md:w-96 // Desktop: side panel
bg-white dark:bg-dark-surface
shadow-lg rounded-t-lg md:rounded-lg
z-[1000] md:z-auto
max-h-[70vh] md:max-h-none // Mobile: don't cover whole screen
overflow-y-auto
transform transition-transform
${isOpen ? 'translate-y-0' : 'translate-y-full md:translate-y-0'}
">
{/* Panel content */}
</div>
// Add drag handle for mobile:
<div className="md:hidden flex justify-center p-2">
<div className="w-12 h-1 bg-gray-300 rounded-full" />
</div>
Touch-friendly buttons:
// Minimum 44x44px touch targets
<button className="
min-w-[44px] min-h-[44px]
px-4 py-2
text-base md:text-sm
">
{/* button content */}
</button>
OPTIONAL ENHANCEMENTS (if time permits)
11. Calculation Stats Display
Show useful statistics after calculation
File: frontend/src/components/map/Legend.tsx
{coveragePoints.length > 0 && (
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm">
<h4 className="font-semibold mb-2">📊 Coverage Statistics</h4>
<div className="space-y-1 text-gray-700 dark:text-gray-300">
<div>Points: {coveragePoints.length.toLocaleString()}</div>
<div>Calculation: {(calculationTime / 1000).toFixed(2)}s</div>
<div>Coverage area: ~{calculateArea()} km²</div>
<div>Sites: {sites.length}</div>
</div>
</div>
)}
12. Templates Verification
Ensure quick templates work correctly
File: frontend/src/components/panels/SiteForm.tsx
const QUICK_TEMPLATES = {
limesdr: {
name: 'LimeSDR Mini',
power: 20,
gain: 2,
frequency: 1800,
height: 10,
antennaType: 'omni' as const
},
'low-bbu': {
name: 'Low Power BBU',
power: 40,
gain: 8,
frequency: 1800,
height: 20,
antennaType: 'omni' as const
},
'high-bbu': {
name: 'High Power BBU',
power: 43,
gain: 15,
frequency: 1800,
height: 30,
antennaType: 'sector' as const,
azimuth: 0,
beamwidth: 65
}
};
const applyTemplate = (templateId: keyof typeof QUICK_TEMPLATES) => {
const template = QUICK_TEMPLATES[templateId];
// Apply ALL fields
setFormData({
...formData,
power: template.power,
gain: template.gain,
frequency: template.frequency,
height: template.height,
antennaType: template.antennaType,
azimuth: template.azimuth || 0,
beamwidth: template.beamwidth || 65
});
toast.success(`Applied: ${template.name}`);
};
TESTING CHECKLIST
After implementation, verify:
Visual Tests:
- Dark theme looks good on all components
- New heatmap gradient is clearly visible (blue=weak, red=strong)
- Legend matches heatmap colors
- Sites list shows frequency, power, antenna type
- Mobile panels don't block map completely
Functional Tests:
- "Save & Calculate" button works (saves + calculates)
- "Save Only" button still works
- Coverage radius slider goes to 100km
- Large radius (50-100km) calculates without errors
- Dark/Light/System theme toggle works
- Theme persists after page refresh
Keyboard Shortcuts:
- Ctrl/Cmd+S saves current site
- Ctrl/Cmd+Enter calculates coverage
- Ctrl/Cmd+N enters placement mode
- Esc cancels/closes
- H toggles heatmap visibility
Mobile Tests (if possible):
- Panels slide up from bottom on mobile
- Touch targets are large enough (44x44px)
- Sliders work with touch
- Map can be panned/zoomed on mobile
Error Handling:
- Helpful error messages for calculation failures
- Toast notifications for all actions
- No console errors
IMPLEMENTATION NOTES
- Start with visual changes (colors, legend) - quick wins
- Then functional changes (button, radius, dark theme)
- Then UX improvements (shortcuts, mobile, errors)
- Test thoroughly - especially dark theme across all components
Performance Note: Current calculation is instant because it's only FSPL without terrain data. When Phase 4 (30m terrain) is implemented, large radius will take longer. Performance warnings can be added then.
FILES TO CREATE/MODIFY
New Files:
frontend/src/store/settings.ts- theme managementfrontend/src/components/ui/ThemeToggle.tsx- theme switcherfrontend/src/hooks/useKeyboardShortcuts.ts- keyboard shortcuts
Modified Files:
frontend/tailwind.config.js- dark mode configfrontend/src/index.css- dark theme CSS variablesfrontend/src/App.tsx- add ThemeToggle, use shortcuts hookfrontend/src/components/map/Heatmap.tsx- new gradientfrontend/src/components/map/Legend.tsx- new colors + statsfrontend/src/components/map/Map.tsx- Fit/Reset buttonsfrontend/src/components/panels/SiteForm.tsx- Save&Calculate, radiusfrontend/src/components/panels/SiteList.tsx- more info displayfrontend/src/store/coverage.ts- better error handling, defaultsfrontend/src/constants/rsrp-thresholds.ts- new color constants- All UI components - add dark mode classes
SUCCESS CRITERIA
✅ New heatmap gradient is clearly visible ✅ Dark theme works flawlessly ✅ Coverage radius goes to 100km ✅ "Save & Calculate" is primary action ✅ Keyboard shortcuts work ✅ Mobile responsive ✅ Better error messages ✅ No TypeScript errors ✅ All existing features still work
Estimated Time: 20-30 minutes for full implementation Complexity: Medium (mostly UI updates + new theme system) Risk: Low (additive changes, not breaking existing functionality)
Good luck! 🚀