@mytec: iter10.1 ready for testing

This commit is contained in:
2026-01-30 16:34:46 +02:00
parent 55fd42b696
commit d0e827e350
5 changed files with 96 additions and 60 deletions

View File

@@ -20,6 +20,7 @@ import ToastContainer from '@/components/ui/Toast.tsx';
import ThemeToggle from '@/components/ui/ThemeToggle.tsx';
import Button from '@/components/ui/Button.tsx';
import NumberInput from '@/components/ui/NumberInput.tsx';
import ConfirmDialog from '@/components/ui/ConfirmDialog.tsx';
const calculator = new RFCalculator();
@@ -57,6 +58,7 @@ export default function App() {
} | null>(null);
const [panelCollapsed, setPanelCollapsed] = useState(false);
const [showShortcuts, setShowShortcuts] = useState(false);
const [kbDeleteTarget, setKbDeleteTarget] = useState<{ id: string; name: string } | null>(null);
// Load sites from IndexedDB on mount
useEffect(() => {
@@ -92,6 +94,44 @@ export default function App() {
setPendingLocation(null);
}, []);
// Keyboard delete confirmation handler
const handleKbDeleteConfirmed = useCallback(async () => {
if (!kbDeleteTarget) return;
const { id, name } = kbDeleteTarget;
const siteData = sites.find((s) => s.id === id);
setKbDeleteTarget(null);
await useSitesStore.getState().deleteSite(id);
if (siteData) {
addToast(`"${name}" deleted`, 'info', {
duration: 10000,
action: {
label: 'Undo',
onClick: async () => {
await useSitesStore.getState().addSite({
name: siteData.name,
lat: siteData.lat,
lon: siteData.lon,
height: siteData.height,
power: siteData.power,
gain: siteData.gain,
frequency: siteData.frequency,
antennaType: siteData.antennaType,
azimuth: siteData.azimuth,
beamwidth: siteData.beamwidth,
color: siteData.color,
visible: siteData.visible,
notes: siteData.notes,
equipment: siteData.equipment,
});
addToast(`"${siteData.name}" restored`, 'success');
},
},
});
}
}, [kbDeleteTarget, sites, addToast]);
// Calculate coverage (with better error handling)
const handleCalculate = useCallback(async () => {
const currentSites = useSitesStore.getState().sites;
@@ -186,6 +226,9 @@ export default function App() {
onCalculate: handleCalculate,
onCloseForm: handleCloseForm,
onShowShortcuts: useCallback(() => setShowShortcuts(true), []),
onDeleteRequest: useCallback((id: string, name: string) => {
setKbDeleteTarget({ id, name });
}, []),
});
return (
@@ -538,6 +581,19 @@ export default function App() {
</div>
</div>
{/* Keyboard delete confirmation dialog */}
{kbDeleteTarget && (
<ConfirmDialog
title="Delete Site?"
message={`Are you sure you want to delete "${kbDeleteTarget.name}"? This action can be undone for 10 seconds.`}
confirmLabel="Delete"
cancelLabel="Cancel"
danger
onConfirm={handleKbDeleteConfirmed}
onCancel={() => setKbDeleteTarget(null)}
/>
)}
<ToastContainer />
</div>
);

View File

@@ -57,10 +57,16 @@ export default memo(function CoverageStats({ points, resolution }: CoverageStats
const totalArea = estimateAreaKm2(points.length, resolution);
const total = points.length;
const rsrpValues = points.map((p) => p.rsrp);
const minRSRP = Math.min(...rsrpValues);
const maxRSRP = Math.max(...rsrpValues);
const avgRSRP = rsrpValues.reduce((a, b) => a + b, 0) / total;
// Use reduce instead of Math.min/max spread — spread crashes on 65k+ elements
let minRSRP = Infinity;
let maxRSRP = -Infinity;
let sumRSRP = 0;
for (const p of points) {
if (p.rsrp < minRSRP) minRSRP = p.rsrp;
if (p.rsrp > maxRSRP) maxRSRP = p.rsrp;
sumRSRP += p.rsrp;
}
const avgRSRP = sumRSRP / total;
// Unique sites contributing to coverage
const uniqueSites = new Set(points.map((p) => p.siteId)).size;

View File

@@ -18,14 +18,14 @@ export function getSignalQuality(rsrp: number): SignalQuality {
return 'no-service';
}
// New color scheme: cold = weak, warm = strong
// Warm color scheme: dark = weak, bright = strong
export const SIGNAL_COLORS: Record<string, string> = {
excellent: '#f44336', // Red (very strong)
excellent: '#ffeb3b', // Yellow (excellent)
good: '#ff9800', // Orange (strong)
fair: '#ffeb3b', // Yellow (acceptable)
poor: '#4caf50', // Green (weak)
weak: '#00bcd4', // Cyan (very weak)
'no-service': '#0d47a1', // Dark Blue (no coverage)
fair: '#ff4444', // Bright red (fair)
poor: '#cc0000', // Red (weak)
weak: '#8b0000', // Dark red (very weak)
'no-service': '#3c1428', // Deep maroon (no coverage)
} as const;
export function getRSRPColor(rsrp: number): string {
@@ -33,10 +33,10 @@ export function getRSRPColor(rsrp: number): string {
}
export const RSRP_LEGEND = [
{ label: 'Excellent', range: '> -70 dBm', color: '#f44336', description: 'Very strong signal', min: -70 },
{ label: 'Excellent', range: '> -70 dBm', color: '#ffeb3b', description: 'Very strong signal', min: -70 },
{ label: 'Good', range: '-70 to -85 dBm', color: '#ff9800', description: 'Strong signal', min: -85 },
{ label: 'Fair', range: '-85 to -100 dBm', color: '#ffeb3b', description: 'Acceptable signal', min: -100 },
{ label: 'Poor', range: '-100 to -110 dBm', color: '#4caf50', description: 'Weak signal', min: -110 },
{ label: 'Weak', range: '-110 to -120 dBm', color: '#00bcd4', description: 'Very weak signal', min: -120 },
{ label: 'No Service', range: '< -120 dBm', color: '#0d47a1', description: 'No coverage', min: -140 },
{ label: 'Fair', range: '-85 to -100 dBm', color: '#ff4444', description: 'Acceptable signal', min: -100 },
{ label: 'Poor', range: '-100 to -110 dBm', color: '#cc0000', description: 'Weak signal', min: -110 },
{ label: 'Weak', range: '-110 to -120 dBm', color: '#8b0000', description: 'Very weak signal', min: -120 },
{ label: 'No Service', range: '< -120 dBm', color: '#3c1428', description: 'No coverage', min: -140 },
] as const;

View File

@@ -8,6 +8,7 @@ interface ShortcutHandlers {
onCalculate: () => void;
onCloseForm: () => void;
onShowShortcuts?: () => void;
onDeleteRequest?: (siteId: string, siteName: string) => void;
}
function isInputActive(): boolean {
@@ -21,6 +22,7 @@ export function useKeyboardShortcuts({
onCalculate,
onCloseForm,
onShowShortcuts,
onDeleteRequest,
}: ShortcutHandlers) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -85,41 +87,15 @@ export function useKeyboardShortcuts({
case 'f': // F: Fit to coverage — dispatch custom event for Map.tsx to handle
window.dispatchEvent(new CustomEvent('rfcp:fit-bounds'));
return;
case 'delete': // Delete key: delete selected site with undo
case 'delete': // Delete key: show confirmation dialog
case 'backspace':
{
const selectedId = useSitesStore.getState().selectedSiteId;
if (selectedId) {
if (selectedId && onDeleteRequest) {
const site = useSitesStore.getState().sites.find(s => s.id === selectedId);
if (site) {
// Snapshot for undo
const snapshot = { ...site };
useSitesStore.getState().deleteSite(selectedId);
useToastStore.getState().addToast(`"${site.name}" deleted`, 'info', {
duration: 10000,
action: {
label: 'Undo',
onClick: async () => {
await useSitesStore.getState().addSite({
name: snapshot.name,
lat: snapshot.lat,
lon: snapshot.lon,
height: snapshot.height,
power: snapshot.power,
gain: snapshot.gain,
frequency: snapshot.frequency,
antennaType: snapshot.antennaType,
azimuth: snapshot.azimuth,
beamwidth: snapshot.beamwidth,
color: snapshot.color,
visible: snapshot.visible,
notes: snapshot.notes,
equipment: snapshot.equipment,
});
useToastStore.getState().addToast(`"${snapshot.name}" restored`, 'success');
},
},
});
e.preventDefault();
onDeleteRequest(site.id, site.name);
}
}
}
@@ -142,5 +118,5 @@ export function useKeyboardShortcuts({
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onCalculate, onCloseForm, onShowShortcuts]);
}, [onCalculate, onCloseForm, onShowShortcuts, onDeleteRequest]);
}

View File

@@ -1,10 +1,10 @@
/**
* RSRP → color mapping with smooth gradient interpolation.
*
* Gradient stops are chosen to match standard RF planning tools:
* -130 dBm = deep blue (no service)
* -90 dBm = green (fair)
* -50 dBm = red (excellent)
* Warm-only palette (no cyan/green):
* -130 dBm = dark maroon (no service)
* -90 dBm = orange (fair)
* -50 dBm = bright red (excellent)
*
* All functions are pure and allocation-free on the hot path
* (pre-built lookup table for fast per-pixel color resolution).
@@ -18,16 +18,14 @@ interface GradientStop {
}
const GRADIENT_STOPS: GradientStop[] = [
{ value: 0.0, r: 26, g: 35, b: 126 }, // #1a237e — deep blue
{ value: 0.15, r: 13, g: 71, b: 161 }, // #0d47a1
{ value: 0.25, r: 33, g: 150, b: 243 }, // #2196f3 — blue
{ value: 0.35, r: 0, g: 188, b: 212 }, // #00bcd4 — cyan
{ value: 0.45, r: 0, g: 137, b: 123 }, // #00897b — teal
{ value: 0.55, r: 76, g: 175, b: 80 }, // #4caf50 — green
{ value: 0.65, r: 139, g: 195, b: 74 }, // #8bc34a — light green
{ value: 0.75, r: 255, g: 235, b: 59 }, // #ffeb3b — yellow
{ value: 0.85, r: 255, g: 152, b: 0 }, // #ff9800 — orange
{ value: 1.0, r: 244, g: 67, b: 54 }, // #f44336 — red
{ value: 0.0, r: 60, g: 20, b: 40 }, // #3c1428 — deep maroon (no service)
{ value: 0.15, r: 100, g: 20, b: 30 }, // #64141e — dark red-brown
{ value: 0.30, r: 139, g: 0, b: 0 }, // #8b0000 — dark red (very weak)
{ value: 0.45, r: 204, g: 0, b: 0 }, // #cc0000 — red (weak)
{ value: 0.60, r: 255, g: 68, b: 68 }, // #ff4444 — bright red (fair)
{ value: 0.75, r: 255, g: 107, b: 53 }, // #ff6b35 — orange-red (good)
{ value: 0.85, r: 255, g: 152, b: 0 }, // #ff9800 — orange (strong)
{ value: 1.0, r: 255, g: 235, b: 59 }, // #ffeb3b — yellow (excellent)
];
/**