Files
rfcp/docs/devlog/front/RFCP-Iteration1-Full-Task.md
2026-01-30 20:39:13 +02:00

848 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```typescript
// 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
```typescript
// 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)
```typescript
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`
```typescript
// 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`
```typescript
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`
```typescript
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 state
- `frontend/src/components/ui/ThemeToggle.tsx` - toggle button
- `frontend/tailwind.config.js` - dark mode config
- `frontend/src/index.css` - dark theme styles
**Implementation:**
**A) Tailwind Config:**
```javascript
// 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:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
// 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:
```typescript
// 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`
```typescript
// 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`
```typescript
// 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:**
```typescript
// 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`
```typescript
// 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`
```typescript
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`
```typescript
// 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:**
```typescript
// 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`
```typescript
{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`
```typescript
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
1. **Start with visual changes** (colors, legend) - quick wins
2. **Then functional changes** (button, radius, dark theme)
3. **Then UX improvements** (shortcuts, mobile, errors)
4. **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 management
- `frontend/src/components/ui/ThemeToggle.tsx` - theme switcher
- `frontend/src/hooks/useKeyboardShortcuts.ts` - keyboard shortcuts
### Modified Files:
- `frontend/tailwind.config.js` - dark mode config
- `frontend/src/index.css` - dark theme CSS variables
- `frontend/src/App.tsx` - add ThemeToggle, use shortcuts hook
- `frontend/src/components/map/Heatmap.tsx` - new gradient
- `frontend/src/components/map/Legend.tsx` - new colors + stats
- `frontend/src/components/map/Map.tsx` - Fit/Reset buttons
- `frontend/src/components/panels/SiteForm.tsx` - Save&Calculate, radius
- `frontend/src/components/panels/SiteList.tsx` - more info display
- `frontend/src/store/coverage.ts` - better error handling, defaults
- `frontend/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! 🚀