@mytec: start iter10.5

This commit is contained in:
2026-01-30 19:30:50 +02:00
parent 24e9591e42
commit f0b62ada77

View File

@@ -0,0 +1,395 @@
# RFCP Iteration 10.5 — Input Validation + Site/Sector Hierarchy UI
**Date:** 2025-01-30
**Status:** Ready for Implementation
**Priority:** Medium
**Estimated Effort:** 45-60 minutes
---
## Overview
This iteration addresses two UX issues:
1. **Bug:** Number input fields don't accept manually typed values
2. **Enhancement:** Site/Sector list needs clear visual hierarchy
---
## Part A: Input Validation Fix
### Problem
When user clears an input field and tries to type a new value (e.g., "15" or "120"), the input doesn't accept it. The field either:
- Resets to minimum value immediately
- Doesn't allow intermediate states (typing "1" before "15")
**Affected Fields:**
- Radius (1-100 km)
- Resolution (50-500 m)
- Min Signal (-140 to -50 dBm)
- Heatmap Opacity (30-100%)
- All numeric inputs in site/sector forms
### Root Cause
The `onChange` handler immediately validates and clamps values to min/max range. When user types "1" (intending to type "15"), it gets clamped or rejected before they can finish typing.
### Solution
**Validate on blur, not on change:**
```typescript
// BEFORE (problematic):
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = Math.max(min, Math.min(max, Number(e.target.value)));
setValue(value);
};
// AFTER (correct):
const [localValue, setLocalValue] = useState(String(value));
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Allow any input while typing
setLocalValue(e.target.value);
};
const handleBlur = () => {
// Validate and clamp only when focus leaves
const parsed = Number(localValue);
if (isNaN(parsed)) {
setLocalValue(String(value)); // Reset to previous valid value
} else {
const clamped = Math.max(min, Math.min(max, parsed));
setLocalValue(String(clamped));
setValue(clamped);
}
};
// Sync local state when prop changes
useEffect(() => {
setLocalValue(String(value));
}, [value]);
```
### Files to Check/Modify
Look for numeric input components in:
- `src/components/panels/SettingsPanel.tsx` (or similar)
- `src/components/ui/NumberInput.tsx` (if exists)
- `src/components/ui/Slider.tsx` (if has input field)
- `src/components/panels/SiteForm.tsx`
### Implementation Pattern
If there's a reusable `NumberInput` component, fix it once. Otherwise, apply the pattern to each input.
**Key Points:**
1. Use local string state for the input value
2. Allow free typing (no validation on change)
3. Validate and clamp on blur
4. Also validate on Enter key press
5. Sync local state when external value changes
```typescript
interface NumberInputProps {
value: number;
onChange: (value: number) => void;
min: number;
max: number;
step?: number;
label?: string;
}
function NumberInput({ value, onChange, min, max, step = 1, label }: NumberInputProps) {
const [localValue, setLocalValue] = useState(String(value));
// Sync when external value changes
useEffect(() => {
setLocalValue(String(value));
}, [value]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocalValue(e.target.value);
};
const commitValue = () => {
const parsed = parseFloat(localValue);
if (isNaN(parsed)) {
setLocalValue(String(value));
return;
}
const clamped = Math.max(min, Math.min(max, parsed));
setLocalValue(String(clamped));
if (clamped !== value) {
onChange(clamped);
}
};
const handleBlur = () => commitValue();
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
commitValue();
(e.target as HTMLInputElement).blur();
}
if (e.key === 'Escape') {
setLocalValue(String(value));
(e.target as HTMLInputElement).blur();
}
};
return (
<input
type="text" // Use text, not number, for better control
inputMode="numeric" // Shows numeric keyboard on mobile
value={localValue}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
);
}
```
---
## Part B: Site/Sector Hierarchy UI
### Problem
Current UI shows flat list with confusing structure:
```
Station-1 2 sectors
Station-1-Beta Edit + Sector ×
Station-1-Beta Edit + Sector ×
```
Issues:
- Sector names duplicate site name ("Station-1-Beta")
- No clear visual hierarchy
- "+ Sector" button on each sector (should be on site only)
- Can't easily distinguish site from sector
### Desired UI
```
📍 Station-1 [Edit Site] [+ Sector] [×]
├─ α Alpha 140° · 2100MHz · 43dBm [Edit] [×]
└─ β Beta 260° · 2100MHz · 43dBm [Edit] [×]
📍 Station-2 [Edit Site] [+ Sector] [×]
└─ α Alpha 90° · 1800MHz · 40dBm [Edit] [×]
```
### Design Specifications
**Site Row:**
- Icon: 📍 or radio tower icon
- Name: Bold, larger font
- Sector count badge: "2 sectors" (subtle)
- Actions: [Edit Site] [+ Sector] [Delete Site]
- Background: Slightly different shade
- Click to expand/collapse sectors
**Sector Row:**
- Indented (padding-left: 24px or similar)
- Greek letter prefix: α, β, γ, δ, ε, ζ (visual identifier)
- Sector name: "Alpha", "Beta" (without site prefix)
- Key params inline: Azimuth · Frequency · Power
- Actions: [Edit] [Delete]
- Tree line connector: ├─ and └─ (optional, CSS pseudo-elements)
**Greek Letters Mapping:**
```typescript
const GREEK_LETTERS = ['α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ'];
// Alpha=α, Beta=β, Gamma=γ, Delta=δ, Epsilon=ε, Zeta=ζ, Eta=η, Theta=θ
```
### Component Structure
```
SiteList
├── SiteItem (for each site)
│ ├── SiteHeader (name, actions, expand/collapse)
│ └── SectorList (when expanded)
│ └── SectorItem (for each sector)
│ ├── SectorInfo (greek letter, name, params)
│ └── SectorActions (edit, delete)
```
### File to Modify
`/opt/rfcp/frontend/src/components/panels/SiteList.tsx`
Or if there are sub-components:
- `SiteItem.tsx`
- `SectorItem.tsx`
### Implementation Notes
**1. Data Structure (existing):**
```typescript
interface Site {
id: string;
name: string;
lat: number;
lon: number;
sectors: Sector[];
}
interface Sector {
id: string;
name: string; // "Alpha", "Beta", etc.
azimuth: number;
frequency: number;
power: number;
// ... other props
}
```
**2. Greek Letter Helper:**
```typescript
const SECTOR_GREEK: Record<string, string> = {
'Alpha': 'α',
'Beta': 'β',
'Gamma': 'γ',
'Delta': 'δ',
'Epsilon': 'ε',
'Zeta': 'ζ',
'Eta': 'η',
'Theta': 'θ',
};
const getGreekLetter = (sectorName: string): string => {
return SECTOR_GREEK[sectorName] || '•';
};
```
**3. CSS for Hierarchy:**
```css
.site-item {
border-left: 3px solid transparent;
}
.site-item.selected {
border-left-color: #7c3aed;
background: rgba(124, 58, 237, 0.1);
}
.site-header {
display: flex;
align-items: center;
padding: 8px 12px;
font-weight: 600;
}
.sector-list {
padding-left: 16px;
border-left: 1px solid rgba(255, 255, 255, 0.1);
margin-left: 12px;
}
.sector-item {
display: flex;
align-items: center;
padding: 6px 12px;
font-size: 0.9em;
}
.sector-greek {
width: 20px;
color: #7c3aed;
font-weight: 600;
}
.sector-params {
color: rgba(255, 255, 255, 0.6);
font-size: 0.85em;
margin-left: 8px;
}
```
**4. Expand/Collapse (optional):**
```typescript
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const toggleExpand = (siteId: string) => {
setExpanded(prev => {
const next = new Set(prev);
if (next.has(siteId)) {
next.delete(siteId);
} else {
next.add(siteId);
}
return next;
});
};
```
---
## Testing Checklist
### Part A: Input Validation
- [ ] Can type "15" in Radius field (not rejected after "1")
- [ ] Can type "120" in Resolution field
- [ ] Value validates on blur (focus out)
- [ ] Value validates on Enter key
- [ ] Escape key reverts to previous value
- [ ] Invalid input (letters) reverts to previous value
- [ ] Values outside range are clamped on blur
- [ ] Slider still works with arrow buttons
- [ ] Mobile: numeric keyboard appears
### Part B: Hierarchy UI
- [ ] Sites show with icon and bold name
- [ ] Sectors indented under their site
- [ ] Greek letters (α, β, γ) show for sectors
- [ ] Sector names without site prefix ("Alpha" not "Station-1-Alpha")
- [ ] Key params visible inline (azimuth, frequency, power)
- [ ] "+ Sector" button only on site row
- [ ] Edit/Delete buttons work correctly
- [ ] Selected site/sector highlighted
- [ ] Multiple sites display correctly
- [ ] Empty state (no sites) handled
---
## Visual Reference
**Before:**
```
┌─────────────────────────────────────────────┐
│ Station-1 2 sectors │
│ ┌─────────────────────────────────────┐ │
│ │ Station-1-Beta Edit +Sector × │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Station-1-Beta Edit +Sector × │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
**After:**
```
┌─────────────────────────────────────────────┐
│ 📍 Station-1 [Edit] [+] [×] │
│ │ │
│ ├─ α Alpha 140° · 2100MHz · 43dBm [✎][×] │
│ └─ β Beta 260° · 2100MHz · 43dBm [✎][×] │
│ │
│ 📍 Station-2 [Edit] [+] [×] │
│ │ │
│ └─ α Alpha 90° · 1800MHz · 40dBm [✎][×] │
└─────────────────────────────────────────────┘
```
---
## Reference
- Previous iteration: RFCP-Iteration10.4-Fix-Stadia-Maps-401.md
- Site/Sector data model: `/opt/rfcp/frontend/src/types/`
- Current SiteList: `/opt/rfcp/frontend/src/components/panels/SiteList.tsx`