1338 lines
28 KiB
Markdown
1338 lines
28 KiB
Markdown
# RFCP - Iteration 10: Final Frontend Audit & Polish
|
||
|
||
**Goal:** Production-ready frontend - zero critical bugs, clean code, polished UX, comprehensive audit.
|
||
|
||
**Status:** Ready for Implementation
|
||
**Estimated Time:** 8-12 hours (can split into 2 sessions)
|
||
|
||
---
|
||
|
||
## 📋 Overview
|
||
|
||
This is the **final frontend iteration** before moving to backend work. After this iteration, the frontend should be:
|
||
- ✅ Bug-free and stable
|
||
- ✅ Well-documented code
|
||
- ✅ TypeScript strict mode compliant
|
||
- ✅ Performance optimized
|
||
- ✅ Production-ready UX
|
||
|
||
---
|
||
|
||
## 🎯 Four-Phase Approach
|
||
|
||
```
|
||
Phase 1: Critical Fixes (P1)
|
||
↓
|
||
Phase 2: Code Audit & Refactor (P1)
|
||
↓
|
||
Phase 3: UX Polish (P2)
|
||
↓
|
||
Phase 4: Final Audit & Testing (P1)
|
||
```
|
||
|
||
---
|
||
|
||
## 📍 Phase 1: Critical Fixes
|
||
|
||
**Priority:** P1 CRITICAL
|
||
**Time:** ~2-3 hours
|
||
|
||
### 1.1 Stack Overflow at 50m Resolution
|
||
|
||
**Problem:** RangeError: Maximum call stack size exceeded
|
||
|
||
**Investigation:**
|
||
```bash
|
||
# Check Web Worker for recursion
|
||
cat public/workers/rf-worker.js
|
||
|
||
# Look for patterns:
|
||
# - Recursive path loss calculation
|
||
# - Infinite loop in terrain processing
|
||
# - Deep call stacks in Fresnel calculations
|
||
```
|
||
|
||
**Likely culprits:**
|
||
1. **Path Loss calculation** - recursive terrain checks
|
||
2. **Fresnel zone** - recursive clearance checks
|
||
3. **LOS calculation** - recursive elevation sampling
|
||
|
||
**Solution pattern:**
|
||
```javascript
|
||
// Add depth guard to any recursive function
|
||
function calculatePathLoss(point, site, depth = 0, maxDepth = 20) {
|
||
if (depth > maxDepth) {
|
||
console.warn('Max recursion depth reached in pathLoss');
|
||
return DEFAULT_PATH_LOSS;
|
||
}
|
||
|
||
// ... calculation logic
|
||
|
||
if (needsRecursion) {
|
||
return calculatePathLoss(newPoint, site, depth + 1, maxDepth);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Files to check:**
|
||
- `public/workers/rf-worker.js`
|
||
- `src/lib/calculator.ts` (if has additional logic)
|
||
- `src/utils/pathLoss.ts` (if exists)
|
||
|
||
**Testing:**
|
||
```bash
|
||
# After fix, test all resolutions:
|
||
50m → ✅ No crash, calculates in <10s
|
||
100m → ✅ Works smoothly
|
||
200m → ✅ Works smoothly
|
||
500m → ✅ Works smoothly
|
||
|
||
# Monitor console for recursion warnings
|
||
```
|
||
|
||
---
|
||
|
||
### 1.2 Green Coverage Radius Circle
|
||
|
||
**Problem:** Green/cyan circle visible, conflicts with RSRP gradient
|
||
|
||
**Investigation:**
|
||
```bash
|
||
# Find all Circle components
|
||
grep -r "Circle" src/ --include="*.tsx"
|
||
|
||
# Check for green/cyan colors
|
||
grep -r "#00ff00\|#00bcd4\|rgb(0,255,0)" src/
|
||
|
||
# Check for dashed circles
|
||
grep -r "dashArray" src/
|
||
```
|
||
|
||
**Likely locations:**
|
||
1. `src/components/map/Map.tsx`
|
||
2. `src/components/map/SiteMarker.tsx`
|
||
3. `src/components/map/CoverageLayer.tsx`
|
||
|
||
**Solution A: Remove completely**
|
||
```tsx
|
||
// Find and DELETE:
|
||
{showCalculationRadius && (
|
||
<Circle
|
||
center={[site.lat, site.lon]}
|
||
radius={site.radius * 1000}
|
||
pathOptions={{ color: '#00bcd4', ... }}
|
||
/>
|
||
)}
|
||
```
|
||
|
||
**Solution B: Change to orange (recommended)**
|
||
```tsx
|
||
<Circle
|
||
center={[site.lat, site.lon]}
|
||
radius={site.radius * 1000}
|
||
pathOptions={{
|
||
color: '#ff9800', // Orange - NOT in RSRP gradient
|
||
weight: 2,
|
||
opacity: 0.6,
|
||
dashArray: '10, 5', // Longer dashes
|
||
fillOpacity: 0
|
||
}}
|
||
/>
|
||
```
|
||
|
||
**Solution C: Make toggleable**
|
||
```tsx
|
||
// In settings panel:
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
checked={showCalculationBounds}
|
||
onChange={(e) => setShowCalculationBounds(e.target.checked)}
|
||
/>
|
||
Show Calculation Bounds
|
||
</label>
|
||
```
|
||
|
||
**Recommended:** Solution B (orange) or C (toggleable)
|
||
|
||
---
|
||
|
||
### 1.3 Any Other Critical Bugs
|
||
|
||
**From user testing:** (TBD based on screenshots/reports)
|
||
|
||
---
|
||
|
||
## 🔍 Phase 2: Code Audit & Refactor
|
||
|
||
**Priority:** P1
|
||
**Time:** ~3-4 hours
|
||
|
||
### 2.1 TypeScript Strict Mode
|
||
|
||
**Enable strict type checking:**
|
||
|
||
**File:** `tsconfig.json`
|
||
```json
|
||
{
|
||
"compilerOptions": {
|
||
"strict": true,
|
||
"noImplicitAny": true,
|
||
"strictNullChecks": true,
|
||
"strictFunctionTypes": true,
|
||
"strictBindCallApply": true,
|
||
"strictPropertyInitialization": true,
|
||
"noImplicitThis": true,
|
||
"alwaysStrict": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**Fix all type errors:**
|
||
```bash
|
||
npm run type-check
|
||
# OR
|
||
npx tsc --noEmit
|
||
```
|
||
|
||
**Common fixes needed:**
|
||
|
||
1. **Replace `any` with proper types:**
|
||
```typescript
|
||
// ❌ Bad
|
||
const data: any = response.data;
|
||
|
||
// ✅ Good
|
||
interface ResponseData {
|
||
sites: Site[];
|
||
coverage: CoveragePoint[];
|
||
}
|
||
const data: ResponseData = response.data;
|
||
```
|
||
|
||
2. **Null checks:**
|
||
```typescript
|
||
// ❌ Bad
|
||
const site = sites.find(s => s.id === id);
|
||
site.name = 'New Name'; // Might be undefined!
|
||
|
||
// ✅ Good
|
||
const site = sites.find(s => s.id === id);
|
||
if (!site) return;
|
||
site.name = 'New Name';
|
||
```
|
||
|
||
3. **Function signatures:**
|
||
```typescript
|
||
// ❌ Bad
|
||
function calculate(sites, settings) { ... }
|
||
|
||
// ✅ Good
|
||
function calculate(
|
||
sites: Site[],
|
||
settings: CoverageSettings
|
||
): CoverageResult { ... }
|
||
```
|
||
|
||
**Files to audit:**
|
||
- `src/store/*.ts` - Zustand stores
|
||
- `src/lib/*.ts` - Utilities
|
||
- `src/components/**/*.tsx` - Components
|
||
- `src/utils/*.ts` - Helpers
|
||
|
||
---
|
||
|
||
### 2.2 ESLint Configuration
|
||
|
||
**Install and configure strict linting:**
|
||
|
||
```bash
|
||
npm install --save-dev eslint-config-airbnb-typescript
|
||
npm install --save-dev @typescript-eslint/eslint-plugin
|
||
npm install --save-dev @typescript-eslint/parser
|
||
```
|
||
|
||
**File:** `eslint.config.js`
|
||
```javascript
|
||
import js from '@eslint/js';
|
||
import typescript from '@typescript-eslint/eslint-plugin';
|
||
import tsParser from '@typescript-eslint/parser';
|
||
import react from 'eslint-plugin-react';
|
||
import reactHooks from 'eslint-plugin-react-hooks';
|
||
|
||
export default [
|
||
js.configs.recommended,
|
||
{
|
||
files: ['**/*.{ts,tsx}'],
|
||
languageOptions: {
|
||
parser: tsParser,
|
||
parserOptions: {
|
||
project: './tsconfig.json',
|
||
},
|
||
},
|
||
plugins: {
|
||
'@typescript-eslint': typescript,
|
||
'react': react,
|
||
'react-hooks': reactHooks,
|
||
},
|
||
rules: {
|
||
// TypeScript
|
||
'@typescript-eslint/no-explicit-any': 'error',
|
||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||
'@typescript-eslint/no-unused-vars': 'error',
|
||
|
||
// React
|
||
'react/prop-types': 'off', // Using TypeScript
|
||
'react-hooks/rules-of-hooks': 'error',
|
||
'react-hooks/exhaustive-deps': 'warn',
|
||
|
||
// General
|
||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||
'prefer-const': 'error',
|
||
'no-var': 'error',
|
||
},
|
||
},
|
||
];
|
||
```
|
||
|
||
**Run lint:**
|
||
```bash
|
||
npm run lint
|
||
npm run lint -- --fix # Auto-fix where possible
|
||
```
|
||
|
||
---
|
||
|
||
### 2.3 Code Organization & Cleanup
|
||
|
||
**Remove dead code:**
|
||
```bash
|
||
# Find unused exports
|
||
npx ts-prune
|
||
|
||
# Remove unused imports (auto)
|
||
npm run lint -- --fix
|
||
```
|
||
|
||
**Consistent naming:**
|
||
- Components: `PascalCase` (SiteForm, HeatmapLegend)
|
||
- Files: Match component name (SiteForm.tsx)
|
||
- Utilities: `camelCase` (calculateDistance, normalizeRSRP)
|
||
- Constants: `UPPER_SNAKE_CASE` (MAX_SITES, DEFAULT_POWER)
|
||
- Types/Interfaces: `PascalCase` (Site, CoverageSettings)
|
||
|
||
**File structure review:**
|
||
```
|
||
src/
|
||
├── components/
|
||
│ ├── map/ # Map-related components
|
||
│ ├── panels/ # Side panels
|
||
│ ├── ui/ # Reusable UI components
|
||
│ └── common/ # Common components
|
||
├── store/ # Zustand stores
|
||
├── lib/ # Business logic
|
||
├── utils/ # Pure utility functions
|
||
├── types/ # TypeScript types
|
||
└── hooks/ # Custom React hooks
|
||
```
|
||
|
||
**Move misplaced files if needed.**
|
||
|
||
---
|
||
|
||
### 2.4 Performance Audit
|
||
|
||
**React.memo for expensive components:**
|
||
|
||
```typescript
|
||
// Components that render frequently but props rarely change
|
||
export default React.memo(HeatmapLegend);
|
||
export default React.memo(SiteMarker);
|
||
export default React.memo(CoverageStats);
|
||
```
|
||
|
||
**useMemo for expensive calculations:**
|
||
```typescript
|
||
const filteredSites = useMemo(
|
||
() => sites.filter(s => s.frequency === selectedFreq),
|
||
[sites, selectedFreq]
|
||
);
|
||
```
|
||
|
||
**useCallback for stable callbacks:**
|
||
```typescript
|
||
const handleSiteUpdate = useCallback(
|
||
(id: string, updates: Partial<Site>) => {
|
||
updateSite(id, updates);
|
||
},
|
||
[updateSite]
|
||
);
|
||
```
|
||
|
||
**Web Worker efficiency:**
|
||
- Verify chunking strategy (currently 4 workers max)
|
||
- Check worker termination (no leaks)
|
||
- Monitor calculation times
|
||
|
||
**Tile cache optimization:**
|
||
```typescript
|
||
// HeatmapTileRenderer.ts
|
||
constructor(radiusMeters = 400, maxCacheSize = 150) {
|
||
// Verify cache size is appropriate
|
||
// Monitor cache hit rate in dev mode
|
||
}
|
||
```
|
||
|
||
**Bundle size check:**
|
||
```bash
|
||
npm run build
|
||
npx vite-bundle-visualizer
|
||
|
||
# Target: <500KB gzipped for main bundle
|
||
```
|
||
|
||
---
|
||
|
||
### 2.5 Error Handling
|
||
|
||
**Add try-catch blocks:**
|
||
|
||
```typescript
|
||
// API calls
|
||
async function fetchCoverageData() {
|
||
try {
|
||
const response = await fetch('/api/coverage');
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('Coverage fetch failed:', error);
|
||
toast.error('Failed to load coverage data. Please try again.');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Worker communication
|
||
worker.onerror = (error) => {
|
||
console.error('Worker error:', error);
|
||
toast.error('Coverage calculation failed. Please try again.');
|
||
terminateWorker();
|
||
};
|
||
|
||
// User actions
|
||
const handleDeleteSite = async (id: string) => {
|
||
try {
|
||
await deleteSite(id);
|
||
toast.success('Site deleted');
|
||
} catch (error) {
|
||
console.error('Delete failed:', error);
|
||
toast.error('Failed to delete site. Please try again.');
|
||
}
|
||
};
|
||
```
|
||
|
||
**Fallback states everywhere:**
|
||
```typescript
|
||
// Loading
|
||
{isLoading && <LoadingSpinner />}
|
||
|
||
// Error
|
||
{error && <ErrorMessage error={error} onRetry={refetch} />}
|
||
|
||
// Empty
|
||
{sites.length === 0 && <EmptyState message="No sites yet" />}
|
||
|
||
// Success
|
||
{sites.length > 0 && <SiteList sites={sites} />}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 Phase 3: UX Polish
|
||
|
||
**Priority:** P2
|
||
**Time:** ~2-3 hours
|
||
|
||
### 3.1 Loading States
|
||
|
||
**Global loading indicator:**
|
||
```tsx
|
||
// App.tsx
|
||
{isCalculating && (
|
||
<div className="loading-overlay">
|
||
<Spinner />
|
||
<p>Calculating coverage...</p>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
**Component-level loading:**
|
||
```tsx
|
||
// SiteList.tsx
|
||
{isLoading ? (
|
||
<LoadingSkeleton />
|
||
) : sites.length === 0 ? (
|
||
<EmptyState />
|
||
) : (
|
||
<SiteListItems sites={sites} />
|
||
)}
|
||
```
|
||
|
||
**Button loading states:**
|
||
```tsx
|
||
<button disabled={isLoading}>
|
||
{isLoading ? (
|
||
<>
|
||
<Spinner size="sm" />
|
||
Calculating...
|
||
</>
|
||
) : (
|
||
'Calculate Coverage'
|
||
)}
|
||
</button>
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 Empty States
|
||
|
||
**No sites:**
|
||
```tsx
|
||
<div className="empty-state">
|
||
<MapIcon size={48} />
|
||
<h3>No Sites Yet</h3>
|
||
<p>Add your first site to start RF coverage planning</p>
|
||
<button onClick={handleAddSite}>
|
||
+ Add Site
|
||
</button>
|
||
</div>
|
||
```
|
||
|
||
**No coverage:**
|
||
```tsx
|
||
<div className="empty-state">
|
||
<RadioIcon size={48} />
|
||
<h3>No Coverage Calculated</h3>
|
||
<p>Click "Calculate Coverage" to see RF heatmap</p>
|
||
</div>
|
||
```
|
||
|
||
**Search no results:**
|
||
```tsx
|
||
<div className="empty-state">
|
||
<SearchIcon size={32} />
|
||
<p>No sites found matching "{searchQuery}"</p>
|
||
<button onClick={clearSearch}>Clear Search</button>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 Error States
|
||
|
||
**Generic error:**
|
||
```tsx
|
||
<div className="error-state">
|
||
<AlertCircle size={48} color="red" />
|
||
<h3>Something Went Wrong</h3>
|
||
<p>{error.message}</p>
|
||
<button onClick={handleRetry}>Try Again</button>
|
||
</div>
|
||
```
|
||
|
||
**Network error:**
|
||
```tsx
|
||
<div className="error-state">
|
||
<WifiOff size={48} />
|
||
<h3>Connection Lost</h3>
|
||
<p>Check your internet connection and try again</p>
|
||
<button onClick={handleRetry}>Retry</button>
|
||
</div>
|
||
```
|
||
|
||
**Validation error:**
|
||
```tsx
|
||
<div className="error-message">
|
||
<AlertTriangle size={16} />
|
||
<span>Power must be between 1-100W</span>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 3.4 Keyboard Shortcuts Help Modal
|
||
|
||
**Create component:**
|
||
|
||
**File:** `src/components/common/KeyboardShortcutsHelp.tsx`
|
||
|
||
```tsx
|
||
import { useEffect, useState } from 'react';
|
||
|
||
const SHORTCUTS = [
|
||
{ key: 'N', description: 'New Site' },
|
||
{ key: 'S', description: 'Save Project' },
|
||
{ key: 'C', description: 'Clone Selected Sector' },
|
||
{ key: 'Delete', description: 'Delete Selected Site' },
|
||
{ key: 'Ctrl+Z', description: 'Undo' },
|
||
{ key: 'Ctrl+Shift+Z', description: 'Redo' },
|
||
{ key: 'Ctrl+F', description: 'Search Sites' },
|
||
{ key: 'Esc', description: 'Close Dialogs' },
|
||
{ key: '?', description: 'Show This Help' },
|
||
];
|
||
|
||
export default function KeyboardShortcutsHelp() {
|
||
const [visible, setVisible] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const handleKeyDown = (e: KeyboardEvent) => {
|
||
if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
|
||
e.preventDefault();
|
||
setVisible(v => !v);
|
||
}
|
||
if (e.key === 'Escape') {
|
||
setVisible(false);
|
||
}
|
||
};
|
||
|
||
window.addEventListener('keydown', handleKeyDown);
|
||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||
}, []);
|
||
|
||
if (!visible) return null;
|
||
|
||
return (
|
||
<div className="modal-overlay" onClick={() => setVisible(false)}>
|
||
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-header">
|
||
<h2>⌨️ Keyboard Shortcuts</h2>
|
||
<button onClick={() => setVisible(false)}>×</button>
|
||
</div>
|
||
|
||
<div className="shortcuts-list">
|
||
{SHORTCUTS.map((shortcut) => (
|
||
<div key={shortcut.key} className="shortcut-row">
|
||
<kbd className="kbd">{shortcut.key}</kbd>
|
||
<span>{shortcut.description}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="modal-footer">
|
||
<button onClick={() => setVisible(false)}>Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
**CSS:**
|
||
```css
|
||
.kbd {
|
||
display: inline-block;
|
||
padding: 3px 8px;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||
min-width: 80px;
|
||
text-align: center;
|
||
}
|
||
|
||
.shortcuts-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.shortcut-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
}
|
||
```
|
||
|
||
**Usage:**
|
||
```tsx
|
||
// App.tsx
|
||
import KeyboardShortcutsHelp from '@/components/common/KeyboardShortcutsHelp';
|
||
|
||
export default function App() {
|
||
return (
|
||
<>
|
||
{/* ... other components */}
|
||
<KeyboardShortcutsHelp />
|
||
</>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.5 Export/Import Project
|
||
|
||
**Export all state to JSON:**
|
||
|
||
**File:** `src/components/panels/ExportPanel.tsx`
|
||
|
||
```tsx
|
||
import { useSitesStore } from '@/store/sites';
|
||
import { useCoverageStore } from '@/store/coverage';
|
||
|
||
export function exportProject() {
|
||
const sites = useSitesStore.getState().sites;
|
||
const settings = useCoverageStore.getState().settings;
|
||
|
||
const project = {
|
||
version: '1.0.0',
|
||
exportDate: new Date().toISOString(),
|
||
sites,
|
||
settings,
|
||
};
|
||
|
||
const blob = new Blob([JSON.stringify(project, null, 2)], {
|
||
type: 'application/json',
|
||
});
|
||
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `rfcp-project-${Date.now()}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
|
||
toast.success('Project exported successfully');
|
||
}
|
||
|
||
export function importProject(file: File) {
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = (e) => {
|
||
try {
|
||
const project = JSON.parse(e.target?.result as string);
|
||
|
||
// Validate version
|
||
if (project.version !== '1.0.0') {
|
||
toast.error('Incompatible project version');
|
||
return;
|
||
}
|
||
|
||
// Import sites
|
||
const { setSites } = useSitesStore.getState();
|
||
setSites(project.sites || []);
|
||
|
||
// Import settings
|
||
const { updateSettings } = useCoverageStore.getState();
|
||
updateSettings(project.settings || {});
|
||
|
||
toast.success('Project imported successfully');
|
||
} catch (error) {
|
||
console.error('Import failed:', error);
|
||
toast.error('Failed to import project');
|
||
}
|
||
};
|
||
|
||
reader.readAsText(file);
|
||
}
|
||
```
|
||
|
||
**UI:**
|
||
```tsx
|
||
// In ProjectPanel or Settings
|
||
<div className="export-import">
|
||
<button onClick={exportProject}>
|
||
📥 Export Project
|
||
</button>
|
||
|
||
<label className="btn-secondary">
|
||
📤 Import Project
|
||
<input
|
||
type="file"
|
||
accept=".json"
|
||
style={{ display: 'none' }}
|
||
onChange={(e) => {
|
||
const file = e.target.files?.[0];
|
||
if (file) importProject(file);
|
||
}}
|
||
/>
|
||
</label>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 3.6 Input Validation
|
||
|
||
**Validate all numeric inputs:**
|
||
|
||
**File:** `src/utils/validation.ts`
|
||
|
||
```typescript
|
||
export interface ValidationRule {
|
||
min?: number;
|
||
max?: number;
|
||
required?: boolean;
|
||
message?: string;
|
||
}
|
||
|
||
export function validateNumber(
|
||
value: number,
|
||
rules: ValidationRule
|
||
): { valid: boolean; error?: string } {
|
||
if (rules.required && (value === null || value === undefined)) {
|
||
return { valid: false, error: rules.message || 'This field is required' };
|
||
}
|
||
|
||
if (rules.min !== undefined && value < rules.min) {
|
||
return { valid: false, error: `Minimum value is ${rules.min}` };
|
||
}
|
||
|
||
if (rules.max !== undefined && value > rules.max) {
|
||
return { valid: false, error: `Maximum value is ${rules.max}` };
|
||
}
|
||
|
||
return { valid: true };
|
||
}
|
||
|
||
// Validation rules
|
||
export const VALIDATION_RULES = {
|
||
power: { min: 1, max: 100, required: true, message: 'Power: 1-100W' },
|
||
gain: { min: 0, max: 30, required: true, message: 'Gain: 0-30 dBi' },
|
||
height: { min: 1, max: 500, required: true, message: 'Height: 1-500m' },
|
||
frequency: { min: 700, max: 3800, required: true, message: 'Freq: 700-3800 MHz' },
|
||
azimuth: { min: 0, max: 359, required: true, message: 'Azimuth: 0-359°' },
|
||
beamwidth: { min: 10, max: 360, required: true, message: 'Beamwidth: 10-360°' },
|
||
radius: { min: 1, max: 100, required: true, message: 'Radius: 1-100 km' },
|
||
resolution: { min: 50, max: 500, required: true, message: 'Resolution: 50-500m' },
|
||
};
|
||
```
|
||
|
||
**Use in NumberInput component:**
|
||
|
||
```tsx
|
||
// NumberInput.tsx
|
||
import { validateNumber, VALIDATION_RULES } from '@/utils/validation';
|
||
|
||
export function NumberInput({ value, onChange, field, ...props }) {
|
||
const rules = VALIDATION_RULES[field];
|
||
const validation = validateNumber(value, rules);
|
||
|
||
return (
|
||
<div className="input-group">
|
||
<input
|
||
type="number"
|
||
value={value}
|
||
onChange={(e) => onChange(parseFloat(e.target.value))}
|
||
className={validation.valid ? '' : 'error'}
|
||
{...props}
|
||
/>
|
||
{!validation.valid && (
|
||
<span className="error-message">
|
||
{validation.error}
|
||
</span>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.7 Toast System Improvements
|
||
|
||
**Enhanced toast with actions:**
|
||
|
||
**File:** `src/components/common/Toast.tsx`
|
||
|
||
```tsx
|
||
export interface ToastAction {
|
||
label: string;
|
||
onClick: () => void;
|
||
}
|
||
|
||
export interface ToastOptions {
|
||
duration?: number;
|
||
action?: ToastAction;
|
||
}
|
||
|
||
export function addToast(
|
||
message: string,
|
||
type: 'success' | 'error' | 'info' | 'warning',
|
||
options?: ToastOptions
|
||
) {
|
||
const id = crypto.randomUUID();
|
||
|
||
const duration = options?.duration || (type === 'error' ? 10000 : 3000);
|
||
|
||
const toast: Toast = {
|
||
id,
|
||
message,
|
||
type,
|
||
action: options?.action,
|
||
duration,
|
||
};
|
||
|
||
// Add to store
|
||
useToastStore.getState().addToast(toast);
|
||
|
||
// Auto-remove after duration
|
||
setTimeout(() => {
|
||
useToastStore.getState().removeToast(id);
|
||
}, duration);
|
||
|
||
return id;
|
||
}
|
||
|
||
// Usage:
|
||
toast.success('Site deleted', {
|
||
duration: 10000,
|
||
action: {
|
||
label: 'Undo',
|
||
onClick: () => restoreSite(deletedSite),
|
||
},
|
||
});
|
||
```
|
||
|
||
**Toast container improvements:**
|
||
```tsx
|
||
// Support multiple toasts
|
||
<div className="toast-container">
|
||
{toasts.map((toast) => (
|
||
<div key={toast.id} className={`toast toast-${toast.type}`}>
|
||
<span>{toast.message}</span>
|
||
{toast.action && (
|
||
<button
|
||
onClick={() => {
|
||
toast.action.onClick();
|
||
removeToast(toast.id);
|
||
}}
|
||
className="toast-action"
|
||
>
|
||
{toast.action.label}
|
||
</button>
|
||
)}
|
||
<button onClick={() => removeToast(toast.id)}>×</button>
|
||
</div>
|
||
))}
|
||
|
||
{toasts.length > 1 && (
|
||
<button
|
||
className="dismiss-all"
|
||
onClick={() => clearAllToasts()}
|
||
>
|
||
Dismiss All ({toasts.length})
|
||
</button>
|
||
)}
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 3.8 Console Cleanup
|
||
|
||
**Remove dev logs in production:**
|
||
|
||
```typescript
|
||
// utils/logger.ts
|
||
const isDev = import.meta.env.DEV;
|
||
|
||
export const logger = {
|
||
log: (...args: unknown[]) => {
|
||
if (isDev) console.log(...args);
|
||
},
|
||
|
||
warn: (...args: unknown[]) => {
|
||
if (isDev) console.warn(...args);
|
||
},
|
||
|
||
error: (...args: unknown[]) => {
|
||
// Always log errors
|
||
console.error(...args);
|
||
},
|
||
|
||
perf: (label: string, fn: () => void) => {
|
||
if (!isDev) return fn();
|
||
|
||
const start = performance.now();
|
||
const result = fn();
|
||
const end = performance.now();
|
||
console.log(`⏱️ ${label}: ${(end - start).toFixed(2)}ms`);
|
||
return result;
|
||
},
|
||
};
|
||
|
||
// Replace all console.log with logger.log
|
||
// Replace all console.warn with logger.warn
|
||
// Keep console.error as-is (or use logger.error)
|
||
```
|
||
|
||
**Find and replace:**
|
||
```bash
|
||
# Find all console.log/warn
|
||
grep -r "console\\.log\\|console\\.warn" src/
|
||
|
||
# Replace with logger (manual or script)
|
||
```
|
||
|
||
---
|
||
|
||
### 3.9 Tooltips for Complex Features
|
||
|
||
**Add tooltips using native `title` or custom component:**
|
||
|
||
```tsx
|
||
// Simple tooltips (native)
|
||
<button title="Calculate RF coverage for all sites">
|
||
Calculate Coverage
|
||
</button>
|
||
|
||
// Or custom tooltip component
|
||
<Tooltip content="Geographic radius in meters for each coverage point">
|
||
<label>Point Radius</label>
|
||
</Tooltip>
|
||
```
|
||
|
||
**Key features needing tooltips:**
|
||
- Coverage Settings (what each setting does)
|
||
- Antenna types (directional vs omnidirectional)
|
||
- RSRP thresholds
|
||
- Resolution vs calculation time tradeoff
|
||
- Clone vs New Site
|
||
|
||
---
|
||
|
||
## ✅ Phase 4: Final Audit & Testing
|
||
|
||
**Priority:** P1
|
||
**Time:** ~1-2 hours
|
||
|
||
### 4.1 Functionality Checklist
|
||
|
||
**Site Management:**
|
||
- [ ] Create new site → success
|
||
- [ ] Edit site parameters → updates correctly
|
||
- [ ] Delete site → confirmation dialog → deleted
|
||
- [ ] Clone sector → creates new sector, not site
|
||
- [ ] Batch operations work (select multiple, delete)
|
||
|
||
**Coverage Calculation:**
|
||
- [ ] Calculate coverage → completes successfully
|
||
- [ ] 50m resolution → no crash
|
||
- [ ] 500m resolution → fast calculation
|
||
- [ ] Progress indicator shows
|
||
- [ ] Results display correctly
|
||
|
||
**Heatmap:**
|
||
- [ ] Geographic scale preserved at all zoom levels
|
||
- [ ] Colors match RSRP legend
|
||
- [ ] Tile rendering <50ms average
|
||
- [ ] Opacity control works
|
||
- [ ] Toggle on/off works
|
||
|
||
**UI/UX:**
|
||
- [ ] Keyboard shortcuts work
|
||
- [ ] Dark mode consistent
|
||
- [ ] No console errors
|
||
- [ ] Loading states show
|
||
- [ ] Error states helpful
|
||
- [ ] Empty states clear
|
||
|
||
**Data Persistence:**
|
||
- [ ] Sites saved to localStorage
|
||
- [ ] Settings persist
|
||
- [ ] Browser refresh preserves state
|
||
- [ ] Export/import works
|
||
|
||
---
|
||
|
||
### 4.2 Performance Checklist
|
||
|
||
**Coverage Calculation:**
|
||
- [ ] <5s for typical scenario (10 sites, 200m resolution, 10km radius)
|
||
- [ ] <30s for heavy scenario (50 sites, 100m resolution, 20km radius)
|
||
- [ ] Web Workers terminate properly (no leaks)
|
||
|
||
**Tile Rendering:**
|
||
- [ ] Average tile render time <50ms
|
||
- [ ] No stuttering during pan/zoom
|
||
- [ ] Cache hit rate >50% after initial load
|
||
|
||
**Memory:**
|
||
- [ ] No memory leaks (check DevTools Memory tab)
|
||
- [ ] Heap size stable after 5 minutes of use
|
||
- [ ] Tile cache doesn't grow unbounded
|
||
|
||
**Bundle Size:**
|
||
- [ ] Main bundle <500KB gzipped
|
||
- [ ] Initial load <2s on 3G
|
||
- [ ] Code splitting working (if applicable)
|
||
|
||
---
|
||
|
||
### 4.3 Cross-Browser Testing
|
||
|
||
**Browsers to test:**
|
||
- [ ] Chrome (latest)
|
||
- [ ] Firefox (latest)
|
||
- [ ] Safari (if on Mac)
|
||
- [ ] Edge (latest)
|
||
|
||
**Mobile (basic):**
|
||
- [ ] Responsive layout works
|
||
- [ ] Touch gestures work
|
||
- [ ] Mobile Chrome/Safari
|
||
|
||
---
|
||
|
||
### 4.4 Edge Cases
|
||
|
||
**Extreme scenarios:**
|
||
- [ ] 0 sites → proper empty state
|
||
- [ ] 1 site → works correctly
|
||
- [ ] 100+ sites → performance acceptable
|
||
- [ ] Overlapping sites → coverage blending correct
|
||
|
||
**Zoom levels:**
|
||
- [ ] Zoom 1 (world) → works
|
||
- [ ] Zoom 8 (city) → works
|
||
- [ ] Zoom 18 (building) → works
|
||
|
||
**Boundary conditions:**
|
||
- [ ] Sites at map edge → no errors
|
||
- [ ] Coverage extends beyond viewport → tiles load
|
||
- [ ] Very large radius (100km) → calculates
|
||
|
||
**Input validation:**
|
||
- [ ] Invalid power (0W, 200W) → error shown
|
||
- [ ] Invalid frequency (100 MHz) → error shown
|
||
- [ ] Negative height → error shown
|
||
- [ ] Empty required fields → error shown
|
||
|
||
---
|
||
|
||
### 4.5 Regression Testing
|
||
|
||
**Features from previous iterations:**
|
||
- [ ] Multi-sector sites work (Iteration 8)
|
||
- [ ] Greek naming (Alpha, Beta, Gamma) correct
|
||
- [ ] Number inputs with sliders work (Iteration 9)
|
||
- [ ] Delete confirmation dialog works (Iteration 9.1)
|
||
- [ ] Undo toast works (Iteration 9.1)
|
||
- [ ] Keyboard shortcuts no conflicts (Iteration 9)
|
||
- [ ] Coverage settings controls work (Iteration 9.1)
|
||
|
||
---
|
||
|
||
## 🚀 Deployment
|
||
|
||
### Build & Test Production Build
|
||
|
||
```bash
|
||
# Clean build
|
||
rm -rf dist/
|
||
npm run build
|
||
|
||
# Test production build locally
|
||
npm run preview
|
||
|
||
# Verify:
|
||
# - No console errors
|
||
# - All features work
|
||
# - Performance good
|
||
# - Assets load correctly
|
||
```
|
||
|
||
### Pre-Deploy Checklist
|
||
|
||
- [ ] All tests pass
|
||
- [ ] TypeScript strict mode clean
|
||
- [ ] ESLint clean (no errors, minimal warnings)
|
||
- [ ] Production build successful
|
||
- [ ] Bundle size acceptable
|
||
- [ ] No TODO/FIXME comments in critical code
|
||
|
||
### Deploy to VPS
|
||
|
||
```bash
|
||
# On local machine
|
||
cd /d/root/rfcp/frontend
|
||
npm run build
|
||
|
||
# Copy to VPS (adjust path/credentials)
|
||
scp -r dist/* user@vps:/opt/rfcp/frontend/dist/
|
||
|
||
# On VPS
|
||
sudo systemctl reload caddy
|
||
```
|
||
|
||
### Post-Deploy Verification
|
||
|
||
```bash
|
||
# Check site loads
|
||
curl -I https://rfcp.eliah.one
|
||
|
||
# Monitor logs
|
||
sudo journalctl -u caddy -f
|
||
|
||
# Test in browser
|
||
# → https://rfcp.eliah.one
|
||
# → All features work
|
||
# → No console errors
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 Documentation Updates
|
||
|
||
### README.md
|
||
|
||
```markdown
|
||
# RFCP - RF Coverage Planning Tool
|
||
|
||
Production-ready tactical communications planning.
|
||
|
||
## Features
|
||
- Multi-site RF coverage calculation
|
||
- Geographic-scale heatmap visualization
|
||
- Professional keyboard shortcuts
|
||
- Real-time coverage analysis
|
||
- Export/Import projects
|
||
|
||
## Tech Stack
|
||
- React 18 + TypeScript
|
||
- Leaflet + custom canvas renderer
|
||
- Web Workers for parallel calculation
|
||
- Zustand state management
|
||
- Vite build system
|
||
|
||
## Quick Start
|
||
\`\`\`bash
|
||
npm install
|
||
npm run dev
|
||
\`\`\`
|
||
|
||
## Build
|
||
\`\`\`bash
|
||
npm run build
|
||
npm run preview # Test production build
|
||
\`\`\`
|
||
```
|
||
|
||
### CHANGELOG.md
|
||
|
||
```markdown
|
||
# Changelog
|
||
|
||
## Iteration 10 - Final Frontend Audit (2026-01-30)
|
||
|
||
### Fixed
|
||
- Stack overflow at 50m resolution
|
||
- Coverage radius circle color (green → orange)
|
||
- TypeScript strict mode compliance
|
||
- All ESLint warnings
|
||
|
||
### Added
|
||
- Keyboard shortcuts help modal (press ?)
|
||
- Export/Import project functionality
|
||
- Input validation with error messages
|
||
- Enhanced toast system with actions
|
||
- Loading/error/empty states everywhere
|
||
|
||
### Improved
|
||
- Code organization and cleanup
|
||
- Performance optimization (React.memo, useMemo)
|
||
- Error handling throughout
|
||
- Console logging (dev/prod separation)
|
||
- Bundle size optimization
|
||
|
||
### Performance
|
||
- Coverage calculation: <5s typical, <30s heavy
|
||
- Tile rendering: <50ms average
|
||
- Bundle size: <500KB gzipped
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 Success Criteria
|
||
|
||
### Must Have (P1):
|
||
- ✅ Zero critical bugs
|
||
- ✅ Zero console errors in production
|
||
- ✅ All features functional
|
||
- ✅ TypeScript strict mode passes
|
||
- ✅ Performance targets met
|
||
- ✅ Code audit complete
|
||
|
||
### Should Have (P2):
|
||
- ✅ Keyboard shortcuts help
|
||
- ✅ Export/Import project
|
||
- ✅ Input validation
|
||
- ✅ Polish all states (loading/error/empty)
|
||
- ✅ Enhanced toast system
|
||
|
||
### Nice to Have (P3):
|
||
- 📱 Mobile responsive improvements
|
||
- 🎨 UI micro-animations
|
||
- 📚 Comprehensive tooltips
|
||
- 🔔 Advanced toast features
|
||
|
||
---
|
||
|
||
## 📊 Iteration Timeline
|
||
|
||
**Session 1 (4-6 hours):**
|
||
- Phase 1: Critical Fixes (2-3h)
|
||
- Phase 2: Code Audit start (2-3h)
|
||
|
||
**Session 2 (4-6 hours):**
|
||
- Phase 2: Code Audit finish (1-2h)
|
||
- Phase 3: UX Polish (2-3h)
|
||
- Phase 4: Final Audit (1-2h)
|
||
|
||
**Total: 8-12 hours**
|
||
|
||
---
|
||
|
||
## 🎬 After Iteration 10
|
||
|
||
### Next Steps:
|
||
1. **Backend Audit** (Iteration 11)
|
||
- API endpoints review
|
||
- Database optimization
|
||
- Error handling
|
||
- Logging setup
|
||
|
||
2. **Documentation** (Iteration 12)
|
||
- Architecture docs
|
||
- Deployment guide
|
||
- API documentation
|
||
- User guide
|
||
|
||
3. **Production Deployment**
|
||
- VPS hardening
|
||
- SSL/HTTPS
|
||
- Monitoring setup
|
||
- Backup strategy
|
||
|
||
---
|
||
|
||
## 🎯 Ready for Implementation!
|
||
|
||
**Priority Order:**
|
||
1. Phase 1 (Critical Fixes) → ASAP
|
||
2. Phase 2 (Code Audit) → Next
|
||
3. Phase 3 (UX Polish) → Then
|
||
4. Phase 4 (Final Audit) → Last
|
||
|
||
**Let's make this frontend production-ready!** 🚀
|
||
|
||
---
|
||
|
||
**Status:** ✅ Specification Complete - Ready for Claude Code
|
||
**Next:** Begin Phase 1 - Critical Fixes
|