# RFCP - Iteration 9.1: Final Polish & Safety ## Issue 1: Coverage Settings Still Use Old Sliders **Problem:** Coverage Settings panel has sliders but not NumberInput components. **Solution:** Replace all sliders with NumberInput. **File:** `frontend/src/App.tsx` (Coverage Settings section) ```typescript import { NumberInput } from '@/components/common/NumberInput'; // Replace: // With: updateSettings({ radius: v })} min={1} max={100} step={1} unit="km" showSlider={true} /> updateSettings({ resolution: v })} min={50} max={500} step={50} unit="m" showSlider={true} /> updateSettings({ rsrpThreshold: v })} min={-140} max={-50} step={5} unit="dBm" showSlider={true} /> setHeatmapOpacity(v / 100)} min={10} max={100} step={5} unit="%" showSlider={true} /> setTerrainOpacity(v / 100)} min={10} max={100} step={5} unit="%" showSlider={true} /> ``` --- ## Issue 2: Calculation Radius Color Confusion **Problem:** Calculation bounds circle is cyan (#00bcd4) - same as weak signal color! **Current:** ```typescript // Blue circle - confuses with RSRP gradient pathOptions: { color: '#00bcd4', // ← Same as weak signal! weight: 2, opacity: 0.5, dashArray: '5, 5' } ``` **Solution:** Use neutral color that's NOT in RSRP gradient. **Option A: Orange (Recommended)** ```typescript pathOptions: { color: '#ff9800', // Orange - clearly different weight: 2, opacity: 0.6, dashArray: '5, 5', fillOpacity: 0 } ``` **Option B: Purple** ```typescript pathOptions: { color: '#9c27b0', // Purple - not in gradient weight: 2, opacity: 0.6, dashArray: '5, 5', fillOpacity: 0 } ``` **Option C: White/Gray** ```typescript pathOptions: { color: '#ffffff', // White weight: 2, opacity: 0.7, dashArray: '5, 5', fillOpacity: 0 } ``` **Recommended: Orange** - highly visible, clearly different from RSRP colors. **File:** Find where calculation bounds Circle/Rectangle is rendered (likely in `Map.tsx` or coverage component) ```typescript // Find this: // Change to: ``` --- ## Issue 3: Delete Confirmation Dialog **Problem:** Accidentally deleting sites with no confirmation! **Solution:** Add confirmation dialog before delete. **File:** `frontend/src/components/common/ConfirmDialog.tsx` (new) ```typescript import { useEffect, useRef } from 'react'; interface ConfirmDialogProps { title: string; message: string; confirmLabel?: string; cancelLabel?: string; onConfirm: () => void; onCancel: () => void; danger?: boolean; } export function ConfirmDialog({ title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', onConfirm, onCancel, danger = false }: ConfirmDialogProps) { const dialogRef = useRef(null); useEffect(() => { // Focus confirm button dialogRef.current?.querySelector('button')?.focus(); // Handle Esc key const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') { onCancel(); } }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); }, [onCancel]); return ( {title} {message} {cancelLabel} {confirmLabel} ); } ``` **CSS:** ```css .confirm-dialog-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; } .confirm-dialog { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 8px; padding: 24px; max-width: 400px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); } .confirm-dialog h3 { margin: 0 0 12px; font-size: 18px; } .confirm-dialog p { margin: 0 0 20px; color: var(--text-secondary); } .dialog-actions { display: flex; gap: 12px; justify-content: flex-end; } .btn-danger { background: #dc2626; color: white; } .btn-danger:hover { background: #b91c1c; } ``` **Usage in SiteList.tsx:** ```typescript const [deleteConfirm, setDeleteConfirm] = useState(null); // Delete button setDeleteConfirm(site.id)}> 🗑️ Delete // Render dialog {deleteConfirm && ( s.id === deleteConfirm)?.name}"? This cannot be undone.`} confirmLabel="Delete" cancelLabel="Cancel" danger={true} onConfirm={async () => { await deleteSite(deleteConfirm); setDeleteConfirm(null); toast.success('Site deleted'); }} onCancel={() => setDeleteConfirm(null)} /> )} ``` --- ## Issue 4: Undo/Redo System **Problem:** Accidents happen - need undo for destructive operations. **Solution:** Simple undo stack with toast action. **File:** `frontend/src/store/history.ts` (new) ```typescript import { create } from 'zustand'; interface HistoryAction { type: 'delete_site' | 'delete_sector' | 'batch_edit'; timestamp: number; data: any; description: string; } interface HistoryState { undoStack: HistoryAction[]; redoStack: HistoryAction[]; pushUndo: (action: HistoryAction) => void; undo: () => HistoryAction | null; redo: () => HistoryAction | null; clear: () => void; } export const useHistoryStore = create((set, get) => ({ undoStack: [], redoStack: [], pushUndo: (action) => { set({ undoStack: [...get().undoStack, action], redoStack: [] // Clear redo stack on new action }); // Limit stack size to 50 actions if (get().undoStack.length > 50) { set({ undoStack: get().undoStack.slice(-50) }); } }, undo: () => { const { undoStack, redoStack } = get(); if (undoStack.length === 0) return null; const action = undoStack[undoStack.length - 1]; set({ undoStack: undoStack.slice(0, -1), redoStack: [...redoStack, action] }); return action; }, redo: () => { const { undoStack, redoStack } = get(); if (redoStack.length === 0) return null; const action = redoStack[redoStack.length - 1]; set({ undoStack: [...undoStack, action], redoStack: redoStack.slice(0, -1) }); return action; }, clear: () => set({ undoStack: [], redoStack: [] }) })); ``` **Integration in sites.ts:** ```typescript import { useHistoryStore } from './history'; const deleteSite = async (id: string) => { const site = sites.find(s => s.id === id); if (!site) return; // Push to undo stack useHistoryStore.getState().pushUndo({ type: 'delete_site', timestamp: Date.now(), data: site, description: `Delete "${site.name}"` }); // Perform delete await db.sites.delete(id); set((state) => ({ sites: state.sites.filter((s) => s.id !== id) })); useCoverageStore.getState().clearCoverage(); }; ``` **Undo Handler:** ```typescript // In App.tsx or main component const handleUndo = async () => { const action = useHistoryStore.getState().undo(); if (!action) { toast.error('Nothing to undo'); return; } switch (action.type) { case 'delete_site': // Restore deleted site await useSitesStore.getState().addSite(action.data); toast.success(`Restored "${action.data.name}"`); break; // ... other action types } }; const handleRedo = async () => { const action = useHistoryStore.getState().redo(); if (!action) { toast.error('Nothing to redo'); return; } switch (action.type) { case 'delete_site': // Re-delete site await useSitesStore.getState().deleteSite(action.data.id); toast.success(`Deleted "${action.data.name}"`); break; } }; ``` **Keyboard Shortcuts:** ```typescript // Add to useKeyboardShortcuts.ts case 'z': if (e.ctrlKey || e.metaKey) { e.preventDefault(); if (e.shiftKey) { handleRedo(); // Ctrl+Shift+Z = Redo } else { handleUndo(); // Ctrl+Z = Undo } } break; ``` **UI Indicator:** ```typescript // Show undo/redo availability const { undoStack, redoStack } = useHistoryStore(); ↶ Undo ↷ Redo ``` --- ## Alternative: Simpler Undo (Toast-based) **Lighter approach without full undo/redo system:** ```typescript const deleteSite = async (id: string) => { const site = sites.find(s => s.id === id); if (!site) return; // Show toast with undo action const toastId = toast.success('Site deleted', { duration: 10000, // 10 seconds to undo action: { label: 'Undo', onClick: async () => { // Restore site await addSite(site); toast.success('Site restored'); } } }); // Perform delete await db.sites.delete(id); set((state) => ({ sites: state.sites.filter((s) => s.id !== id) })); }; ``` This is **much simpler** and covers 90% of use cases! --- ## Recommended Priority **Priority 1 (Must Fix):** 1. ✅ Coverage Settings → NumberInput (consistency) 2. ✅ Calculation radius color → Orange (clarity) 3. ✅ Delete confirmation dialog (safety) **Priority 2 (Nice to Have):** 4. ⭐ Toast-based undo (simple, effective) **Priority 3 (Future):** 5. 🔮 Full undo/redo system (complex, might be overkill) --- ## Testing ### Coverage Settings: - [ ] All sliders replaced with NumberInput - [ ] Can type exact values (e.g., "73 km") - [ ] Arrows work (+/- 1) - [ ] Slider still works ### Calculation Radius: - [ ] Circle is orange (not cyan) - [ ] Clearly different from RSRP gradient - [ ] Visible on all map styles ### Delete Confirmation: - [ ] Click delete → dialog appears - [ ] Can cancel (Esc key works) - [ ] Can confirm → site deleted - [ ] No accidental deletes ### Undo: - [ ] Delete site → toast with "Undo" button - [ ] Click Undo → site restored - [ ] Ctrl+Z also works - [ ] Undo expires after 10s --- ## Build & Deploy ```bash cd /opt/rfcp/frontend npm run build sudo systemctl reload caddy ``` --- ## Commit Message ``` fix(ux): coverage settings number inputs, calc radius color - Replaced Coverage Settings sliders with NumberInput components - Changed calculation radius color: cyan → orange (avoid RSRP conflict) - Added delete confirmation dialog with Esc key support - Implemented toast-based undo for delete operations (10s window) Prevents accidental data loss and improves input consistency. ``` 🚀 Ready for Iteration 9.1!
{message}