Fix map not visible when both sidebars are hidden

- Add dynamic grid columns that adjust based on which sidebars are visible
- Add panel visibility settings in Settings panel to toggle individual panels
- Add panels config to default config and config loader
- Add invalidateSize() call to map when panels toggle (monolithic version)
- Add Playwright as dev dependency for testing
pull/103/head
Brian Keating 6 months ago
parent a3f753c0dd
commit 7dc0fd99ec

@ -27,6 +27,7 @@
"ws": "^8.14.2" "ws": "^8.14.2"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -44,4 +45,4 @@
], ],
"author": "K0CJH", "author": "K0CJH",
"license": "MIT" "license": "MIT"
} }

@ -2289,7 +2289,7 @@
// ============================================ // ============================================
// LEAFLET MAP COMPONENT // LEAFLET MAP COMPONENT
// ============================================ // ============================================
const WorldMap = ({ deLocation, dxLocation, onDXChange, potaSpots, mySpots, dxPaths, dxFilters, satellites, showDXPaths, showDXLabels, onToggleDXLabels, showPOTA, showSatellites, onToggleSatellites, hoveredSpot }) => { const WorldMap = ({ deLocation, dxLocation, onDXChange, potaSpots, mySpots, dxPaths, dxFilters, satellites, showDXPaths, showDXLabels, onToggleDXLabels, showPOTA, showSatellites, onToggleSatellites, hoveredSpot, showLeftPanel, showRightPanel }) => {
const mapRef = useRef(null); const mapRef = useRef(null);
const mapInstanceRef = useRef(null); const mapInstanceRef = useRef(null);
const tileLayerRef = useRef(null); const tileLayerRef = useRef(null);
@ -2420,6 +2420,16 @@
} }
}, [mapStyle]); }, [mapStyle]);
useEffect(() => {
if (!mapInstanceRef.current) return;
const timer = setTimeout(() => {
if (mapInstanceRef.current) {
mapInstanceRef.current.invalidateSize();
}
}, 100);
return () => clearTimeout(timer);
}, [showLeftPanel, showRightPanel]);
// Update markers and path // Update markers and path
useEffect(() => { useEffect(() => {
if (!mapInstanceRef.current) return; if (!mapInstanceRef.current) return;
@ -5072,7 +5082,34 @@
const toggleDXLabels = useCallback(() => setMapLayers(prev => ({ ...prev, showDXLabels: !prev.showDXLabels })), []); const toggleDXLabels = useCallback(() => setMapLayers(prev => ({ ...prev, showDXLabels: !prev.showDXLabels })), []);
const togglePOTA = useCallback(() => setMapLayers(prev => ({ ...prev, showPOTA: !prev.showPOTA })), []); const togglePOTA = useCallback(() => setMapLayers(prev => ({ ...prev, showPOTA: !prev.showPOTA })), []);
const toggleSatellites = useCallback(() => setMapLayers(prev => ({ ...prev, showSatellites: !prev.showSatellites })), []); const toggleSatellites = useCallback(() => setMapLayers(prev => ({ ...prev, showSatellites: !prev.showSatellites })), []);
const [showLeftPanel, setShowLeftPanel] = useState(() => {
try {
const stored = localStorage.getItem('openhamclock_showLeftPanel');
return stored === 'true';
} catch (e) { return true; }
});
const [showRightPanel, setShowRightPanel] = useState(() => {
try {
const stored = localStorage.getItem('openhamclock_showRightPanel');
return stored === 'true';
} catch (e) { return true; }
});
useEffect(() => {
try {
localStorage.setItem('openhamclock_showLeftPanel', showLeftPanel.toString());
} catch (e) { console.error('Failed to save left panel visibility:', e); }
}, [showLeftPanel]);
useEffect(() => {
try {
localStorage.setItem('openhamclock_showRightPanel', showRightPanel.toString());
} catch (e) { console.error('Failed to save right panel visibility:', e); }
}, [showRightPanel]);
const toggleLeftPanel = useCallback(() => setShowLeftPanel(prev => !prev), []);
const toggleRightPanel = useCallback(() => setShowRightPanel(prev => !prev), []);
// 12/24 hour format preference with localStorage persistence // 12/24 hour format preference with localStorage persistence
const [use12Hour, setUse12Hour] = useState(() => { const [use12Hour, setUse12Hour] = useState(() => {
try { try {
@ -5298,7 +5335,7 @@
transform: `scale(${scale})`, transform: `scale(${scale})`,
transformOrigin: 'center center', transformOrigin: 'center center',
display: 'grid', display: 'grid',
gridTemplateColumns: '280px 1fr 280px', gridTemplateColumns: `${showLeftPanel ? '280px' : '0'} 1fr ${showRightPanel ? '280px' : '0'}`,
gridTemplateRows: '50px 1fr', gridTemplateRows: '50px 1fr',
gap: '8px', gap: '8px',
padding: '8px', padding: '8px',
@ -5359,9 +5396,35 @@
<div><span style={{ color: 'var(--text-muted)' }}>K </span><span style={{ color: spaceWeather.data?.kIndex >= 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '600' }}>{spaceWeather.data?.kIndex ?? '--'}</span></div> <div><span style={{ color: 'var(--text-muted)' }}>K </span><span style={{ color: spaceWeather.data?.kIndex >= 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '600' }}>{spaceWeather.data?.kIndex ?? '--'}</span></div>
<div><span style={{ color: 'var(--text-muted)' }}>SSN </span><span style={{ color: 'var(--accent-cyan)', fontWeight: '600' }}>{spaceWeather.data?.sunspotNumber || '--'}</span></div> <div><span style={{ color: 'var(--text-muted)' }}>SSN </span><span style={{ color: 'var(--accent-cyan)', fontWeight: '600' }}>{spaceWeather.data?.sunspotNumber || '--'}</span></div>
</div> </div>
{/* Settings & Fullscreen Buttons */} {/* Settings & Fullscreen Buttons */}
<div style={{ display: 'flex', gap: '8px' }}> <div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={toggleLeftPanel}
style={{
background: showLeftPanel ? 'rgba(0, 255, 136, 0.15)' : 'var(--bg-tertiary)',
border: `1px solid ${showLeftPanel ? 'var(--accent-green)' : 'var(--border-color)'}`,
padding: '6px 12px', borderRadius: '4px',
color: showLeftPanel ? 'var(--accent-green)' : 'var(--text-secondary)',
fontSize: '13px', cursor: 'pointer'
}}
title={showLeftPanel ? "Hide Left Panel" : "Show Left Panel"}
>
◀ L
</button>
<button
onClick={toggleRightPanel}
style={{
background: showRightPanel ? 'rgba(0, 255, 136, 0.15)' : 'var(--bg-tertiary)',
border: `1px solid ${showRightPanel ? 'var(--accent-green)' : 'var(--border-color)'}`,
padding: '6px 12px', borderRadius: '4px',
color: showRightPanel ? 'var(--accent-green)' : 'var(--text-secondary)',
fontSize: '13px', cursor: 'pointer'
}}
title={showRightPanel ? "Hide Right Panel" : "Show Right Panel"}
>
R ▶
</button>
<a <a
href="https://buymeacoffee.com/k0cjh" href="https://buymeacoffee.com/k0cjh"
target="_blank" target="_blank"
@ -5401,7 +5464,7 @@
</div> </div>
{/* LEFT SIDEBAR */} {/* LEFT SIDEBAR */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflowY: 'auto', overflowX: 'hidden' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflowY: 'auto', overflowX: 'hidden', visibility: showLeftPanel ? 'visible' : 'hidden' }}>
{/* DE Location */} {/* DE Location */}
<div className="panel" style={{ padding: '12px', flex: '0 0 auto' }}> <div className="panel" style={{ padding: '12px', flex: '0 0 auto' }}>
<div style={{ fontSize: '13px', color: 'var(--accent-cyan)', fontWeight: '700', marginBottom: '8px' }}>📍 DE - YOUR LOCATION</div> <div style={{ fontSize: '13px', color: 'var(--accent-cyan)', fontWeight: '700', marginBottom: '8px' }}>📍 DE - YOUR LOCATION</div>
@ -5441,12 +5504,12 @@
{/* CENTER - MAP */} {/* CENTER - MAP */}
<div style={{ position: 'relative', borderRadius: '6px', overflow: 'hidden' }}> <div style={{ position: 'relative', borderRadius: '6px', overflow: 'hidden' }}>
<WorldMap <WorldMap
deLocation={config.location} deLocation={config.location}
dxLocation={dxLocation} dxLocation={dxLocation}
onDXChange={handleDXChange} onDXChange={handleDXChange}
potaSpots={potaSpots.data} potaSpots={potaSpots.data}
mySpots={mySpots.data} mySpots={mySpots.data}
dxPaths={dxPaths.data} dxPaths={dxPaths.data}
dxFilters={dxFilters} dxFilters={dxFilters}
satellites={satellites.positions} satellites={satellites.positions}
@ -5457,6 +5520,8 @@
showSatellites={mapLayers.showSatellites} showSatellites={mapLayers.showSatellites}
onToggleSatellites={toggleSatellites} onToggleSatellites={toggleSatellites}
hoveredSpot={hoveredSpot} hoveredSpot={hoveredSpot}
showLeftPanel={showLeftPanel}
showRightPanel={showRightPanel}
/> />
<div style={{ position: 'absolute', bottom: '8px', left: '50%', transform: 'translateX(-50%)', fontSize: '13px', color: 'var(--text-muted)', background: 'rgba(0,0,0,0.7)', padding: '2px 8px', borderRadius: '4px' }}> <div style={{ position: 'absolute', bottom: '8px', left: '50%', transform: 'translateX(-50%)', fontSize: '13px', color: 'var(--text-muted)', background: 'rgba(0,0,0,0.7)', padding: '2px 8px', borderRadius: '4px' }}>
Click map to set DX • 73 de {config.callsign} Click map to set DX • 73 de {config.callsign}
@ -5464,7 +5529,7 @@
</div> </div>
{/* RIGHT SIDEBAR */} {/* RIGHT SIDEBAR */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflow: 'hidden' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflow: 'hidden', visibility: showRightPanel ? 'visible' : 'hidden' }}>
{/* DX Cluster - Compact with filters */} {/* DX Cluster - Compact with filters */}
<div className="panel" style={{ padding: '10px', flex: '1 1 auto', overflow: 'hidden', minHeight: 0 }}> <div className="panel" style={{ padding: '10px', flex: '1 1 auto', overflow: 'hidden', minHeight: 0 }}>
<div style={{ fontSize: '12px', color: 'var(--accent-green)', fontWeight: '700', marginBottom: '6px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ fontSize: '12px', color: 'var(--accent-green)', fontWeight: '700', marginBottom: '6px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>

@ -278,6 +278,33 @@ const App = () => {
const localTime = currentTime.toLocaleTimeString('en-US', localTimeOpts); const localTime = currentTime.toLocaleTimeString('en-US', localTimeOpts);
const localDate = currentTime.toLocaleDateString('en-US', localDateOpts); const localDate = currentTime.toLocaleDateString('en-US', localDateOpts);
// Calculate sidebar visibility for responsive grid
const leftSidebarVisible = config.panels?.deLocation?.visible !== false ||
config.panels?.dxLocation?.visible !== false ||
config.panels?.solar?.visible !== false ||
config.panels?.propagation?.visible !== false;
const rightSidebarVisible = config.panels?.dxCluster?.visible !== false ||
config.panels?.pskReporter?.visible !== false ||
config.panels?.dxpeditions?.visible !== false ||
config.panels?.pota?.visible !== false ||
config.panels?.contests?.visible !== false;
const leftSidebarWidth = leftSidebarVisible ? '270px' : '0px';
const rightSidebarWidth = rightSidebarVisible ? '300px' : '0px';
// Dynamic grid columns - adjust based on which sidebars are visible
const getGridTemplateColumns = () => {
if (!leftSidebarVisible && !rightSidebarVisible) {
return '1fr'; // Only map visible - single column
}
if (!leftSidebarVisible) {
return `1fr ${rightSidebarWidth}`; // Only right sidebar
}
if (!rightSidebarVisible) {
return `${leftSidebarWidth} 1fr`; // Only left sidebar
}
return `${leftSidebarWidth} 1fr ${rightSidebarWidth}`; // Both sidebars
};
// Scale for small screens // Scale for small screens
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
useEffect(() => { useEffect(() => {
@ -580,10 +607,10 @@ const App = () => {
transform: `scale(${scale})`, transform: `scale(${scale})`,
transformOrigin: 'center center', transformOrigin: 'center center',
display: 'grid', display: 'grid',
gridTemplateColumns: '270px 1fr 300px', gridTemplateColumns: getGridTemplateColumns(),
gridTemplateRows: '55px 1fr', gridTemplateRows: '55px 1fr',
gap: '8px', gap: leftSidebarVisible || rightSidebarVisible ? '8px' : '0',
padding: '8px', padding: leftSidebarVisible || rightSidebarVisible ? '8px' : '0',
overflow: 'hidden', overflow: 'hidden',
boxSizing: 'border-box' boxSizing: 'border-box'
}}> }}>
@ -604,217 +631,227 @@ const App = () => {
/> />
{/* LEFT SIDEBAR */} {/* LEFT SIDEBAR */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflowY: 'auto', overflowX: 'hidden' }}> {leftSidebarVisible && (
{/* DE Location + Weather */} <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflowY: 'auto', overflowX: 'hidden' }}>
<div className="panel" style={{ padding: '14px', flex: '0 0 auto' }}> {/* DE Location + Weather */}
<div style={{ fontSize: '14px', color: 'var(--accent-cyan)', fontWeight: '700', marginBottom: '10px' }}>📍 DE - YOUR LOCATION</div> {config.panels?.deLocation?.visible !== false && (
<div style={{ fontFamily: 'JetBrains Mono', fontSize: '14px' }}> <div className="panel" style={{ padding: '14px', flex: '0 0 auto' }}>
<div style={{ color: 'var(--accent-amber)', fontSize: '22px', fontWeight: '700', letterSpacing: '1px' }}>{deGrid}</div> <div style={{ fontSize: '14px', color: 'var(--accent-cyan)', fontWeight: '700', marginBottom: '10px' }}>📍 DE - YOUR LOCATION</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '13px', marginTop: '4px' }}>{config.location.lat.toFixed(4)}°, {config.location.lon.toFixed(4)}°</div> <div style={{ fontFamily: 'JetBrains Mono', fontSize: '14px' }}>
<div style={{ marginTop: '8px', fontSize: '13px' }}> <div style={{ color: 'var(--accent-amber)', fontSize: '22px', fontWeight: '700', letterSpacing: '1px' }}>{deGrid}</div>
<span style={{ color: 'var(--text-secondary)' }}> </span> <div style={{ color: 'var(--text-secondary)', fontSize: '13px', marginTop: '4px' }}>{config.location.lat.toFixed(4)}°, {config.location.lon.toFixed(4)}°</div>
<span style={{ color: 'var(--accent-amber)', fontWeight: '600' }}>{deSunTimes.sunrise}</span> <div style={{ marginTop: '8px', fontSize: '13px' }}>
<span style={{ color: 'var(--text-secondary)' }}> </span> <span style={{ color: 'var(--text-secondary)' }}> </span>
<span style={{ color: 'var(--accent-purple)', fontWeight: '600' }}>{deSunTimes.sunset}</span> <span style={{ color: 'var(--accent-amber)', fontWeight: '600' }}>{deSunTimes.sunrise}</span>
</div> <span style={{ color: 'var(--text-secondary)' }}> </span>
</div> <span style={{ color: 'var(--accent-purple)', fontWeight: '600' }}>{deSunTimes.sunset}</span>
{/* Local Weather — compact by default, click to expand */}
{localWeather.data && (() => {
const w = localWeather.data;
const deg = `°${w.tempUnit || tempUnit}`;
const wind = w.windUnit || 'mph';
const vis = w.visUnit || 'mi';
return (
<div style={{ marginTop: '12px', borderTop: '1px solid var(--border-color)', paddingTop: '10px' }}>
{/* Compact summary row — always visible */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
onClick={() => { const next = !weatherExpanded; setWeatherExpanded(next); try { localStorage.setItem('openhamclock_weatherExpanded', next.toString()); } catch {} }}
style={{
display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer',
userSelect: 'none', flex: 1, minWidth: 0,
}}
>
<span style={{ fontSize: '20px', lineHeight: 1 }}>{w.icon}</span>
<span style={{ fontSize: '18px', fontWeight: '700', color: 'var(--text-primary)', fontFamily: 'Orbitron, monospace' }}>
{w.temp}{deg}
</span>
<span style={{ fontSize: '11px', color: 'var(--text-secondary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{w.description}</span>
<span style={{ fontSize: '11px', color: 'var(--text-muted)', fontFamily: 'JetBrains Mono, monospace' }}>
💨{w.windSpeed}
</span>
<span style={{
fontSize: '10px', color: 'var(--text-muted)',
transform: weatherExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s',
}}></span>
</div>
{/* F/C toggle */}
<button
onClick={(e) => {
e.stopPropagation();
const next = tempUnit === 'F' ? 'C' : 'F';
setTempUnit(next);
try { localStorage.setItem('openhamclock_tempUnit', next); } catch {}
}}
style={{
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
color: 'var(--text-secondary)',
fontSize: '10px',
padding: '1px 5px',
borderRadius: '3px',
cursor: 'pointer',
fontFamily: 'JetBrains Mono, monospace',
fontWeight: '600',
flexShrink: 0,
}}
title={`Switch to °${tempUnit === 'F' ? 'C' : 'F'}`}
>
°{tempUnit === 'F' ? 'C' : 'F'}
</button>
</div> </div>
</div>
{/* Expanded details */}
{weatherExpanded && ( {/* Local Weather — compact by default, click to expand */}
<div style={{ marginTop: '10px' }}> {localWeather.data && (() => {
{/* Feels like + hi/lo */} const w = localWeather.data;
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', marginBottom: '8px', fontFamily: 'JetBrains Mono, monospace' }}> const deg = `°${w.tempUnit || tempUnit}`;
{w.feelsLike !== w.temp && ( const wind = w.windUnit || 'mph';
<span style={{ color: 'var(--text-muted)' }}>Feels like {w.feelsLike}{deg}</span> const vis = w.visUnit || 'mi';
)} return (
{w.todayHigh != null && ( <div style={{ marginTop: '12px', borderTop: '1px solid var(--border-color)', paddingTop: '10px' }}>
<span style={{ color: 'var(--text-muted)', marginLeft: 'auto' }}> {/* Compact summary row — always visible */}
<span style={{ color: 'var(--accent-amber)' }}>{w.todayHigh}°</span> <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{' '} <div
<span style={{ color: 'var(--accent-blue)' }}>{w.todayLow}°</span> onClick={() => { const next = !weatherExpanded; setWeatherExpanded(next); try { localStorage.setItem('openhamclock_weatherExpanded', next.toString()); } catch {} }}
</span> style={{
)} display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer',
userSelect: 'none', flex: 1, minWidth: 0,
}}
>
<span style={{ fontSize: '20px', lineHeight: 1 }}>{w.icon}</span>
<span style={{ fontSize: '18px', fontWeight: '700', color: 'var(--text-primary)', fontFamily: 'Orbitron, monospace' }}>
{w.temp}{deg}
</span>
<span style={{ fontSize: '11px', color: 'var(--text-secondary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{w.description}</span>
<span style={{ fontSize: '11px', color: 'var(--text-muted)', fontFamily: 'JetBrains Mono, monospace' }}>
💨{w.windSpeed}
</span>
<span style={{
fontSize: '10px', color: 'var(--text-muted)',
transform: weatherExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s',
}}></span>
</div> </div>
{/* F/C toggle */}
{/* Detail grid */} <button
<div style={{ onClick={(e) => {
display: 'grid', e.stopPropagation();
gridTemplateColumns: '1fr 1fr', const next = tempUnit === 'F' ? 'C' : 'F';
gap: '6px 12px', setTempUnit(next);
fontSize: '11px', try { localStorage.setItem('openhamclock_tempUnit', next); } catch {}
fontFamily: 'JetBrains Mono, monospace', }}
}}> style={{
<div style={{ display: 'flex', justifyContent: 'space-between' }}> background: 'var(--bg-tertiary)',
<span style={{ color: 'var(--text-muted)' }}>💨 Wind</span> border: '1px solid var(--border-color)',
<span style={{ color: 'var(--text-secondary)' }}>{w.windDir} {w.windSpeed} {wind}</span> color: 'var(--text-secondary)',
</div> fontSize: '10px',
<div style={{ display: 'flex', justifyContent: 'space-between' }}> padding: '1px 5px',
<span style={{ color: 'var(--text-muted)' }}>💧 Humidity</span> borderRadius: '3px',
<span style={{ color: 'var(--text-secondary)' }}>{w.humidity}%</span> cursor: 'pointer',
fontFamily: 'JetBrains Mono, monospace',
fontWeight: '600',
flexShrink: 0,
}}
title={`Switch to °${tempUnit === 'F' ? 'C' : 'F'}`}
>
°{tempUnit === 'F' ? 'C' : 'F'}
</button>
</div>
{/* Expanded details */}
{weatherExpanded && (
<div style={{ marginTop: '10px' }}>
{/* Feels like + hi/lo */}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', marginBottom: '8px', fontFamily: 'JetBrains Mono, monospace' }}>
{w.feelsLike !== w.temp && (
<span style={{ color: 'var(--text-muted)' }}>Feels like {w.feelsLike}{deg}</span>
)}
{w.todayHigh != null && (
<span style={{ color: 'var(--text-muted)', marginLeft: 'auto' }}>
<span style={{ color: 'var(--accent-amber)' }}>{w.todayHigh}°</span>
{' '}
<span style={{ color: 'var(--accent-blue)' }}>{w.todayLow}°</span>
</span>
)}
</div> </div>
{w.windGusts > w.windSpeed + 5 && (
{/* Detail grid */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '6px 12px',
fontSize: '11px',
fontFamily: 'JetBrains Mono, monospace',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🌬 Gusts</span> <span style={{ color: 'var(--text-muted)' }}>💨 Wind</span>
<span style={{ color: 'var(--text-secondary)' }}>{w.windGusts} {wind}</span> <span style={{ color: 'var(--text-secondary)' }}>{w.windDir} {w.windSpeed} {wind}</span>
</div> </div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🌡 Dew Pt</span>
<span style={{ color: 'var(--text-secondary)' }}>{w.dewPoint}{deg}</span>
</div>
{w.pressure && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🔵 Pressure</span> <span style={{ color: 'var(--text-muted)' }}>💧 Humidity</span>
<span style={{ color: 'var(--text-secondary)' }}>{w.pressure} hPa</span> <span style={{ color: 'var(--text-secondary)' }}>{w.humidity}%</span>
</div> </div>
)} {w.windGusts > w.windSpeed + 5 && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}> Clouds</span> <span style={{ color: 'var(--text-muted)' }}>🌬 Gusts</span>
<span style={{ color: 'var(--text-secondary)' }}>{w.cloudCover}%</span> <span style={{ color: 'var(--text-secondary)' }}>{w.windGusts} {wind}</span>
</div> </div>
{w.visibility && ( )}
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>👁 Vis</span> <span style={{ color: 'var(--text-muted)' }}>🌡 Dew Pt</span>
<span style={{ color: 'var(--text-secondary)' }}>{w.visibility} {vis}</span> <span style={{ color: 'var(--text-secondary)' }}>{w.dewPoint}{deg}</span>
</div> </div>
)} {w.pressure && (
{w.uvIndex > 0 && ( <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>🔵 Pressure</span>
<span style={{ color: 'var(--text-secondary)' }}>{w.pressure} hPa</span>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}> UV</span> <span style={{ color: 'var(--text-muted)' }}> Clouds</span>
<span style={{ color: w.uvIndex >= 8 ? '#ef4444' : w.uvIndex >= 6 ? '#f97316' : w.uvIndex >= 3 ? '#eab308' : 'var(--text-secondary)' }}> <span style={{ color: 'var(--text-secondary)' }}>{w.cloudCover}%</span>
{w.uvIndex.toFixed(1)}
</span>
</div> </div>
)} {w.visibility && (
</div> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)' }}>👁 Vis</span>
{/* 3-Day Forecast */} <span style={{ color: 'var(--text-secondary)' }}>{w.visibility} {vis}</span>
{w.daily?.length > 0 && ( </div>
<div style={{ )}
marginTop: '10px', {w.uvIndex > 0 && (
paddingTop: '8px', <div style={{ display: 'flex', justifyContent: 'space-between' }}>
borderTop: '1px solid var(--border-color)', <span style={{ color: 'var(--text-muted)' }}> UV</span>
}}> <span style={{ color: w.uvIndex >= 8 ? '#ef4444' : w.uvIndex >= 6 ? '#f97316' : w.uvIndex >= 3 ? '#eab308' : 'var(--text-secondary)' }}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginBottom: '6px', fontWeight: '600' }}>FORECAST</div> {w.uvIndex.toFixed(1)}
<div style={{ display: 'flex', gap: '4px' }}> </span>
{w.daily.map((day, i) => ( </div>
<div key={i} style={{ )}
flex: 1, </div>
textAlign: 'center',
padding: '6px 2px', {/* 3-Day Forecast */}
background: 'var(--bg-tertiary)', {w.daily?.length > 0 && (
borderRadius: '4px', <div style={{
fontSize: '10px', marginTop: '10px',
}}> paddingTop: '8px',
<div style={{ color: 'var(--text-muted)', fontWeight: '600', marginBottom: '2px' }}>{i === 0 ? 'Today' : day.date}</div> borderTop: '1px solid var(--border-color)',
<div style={{ fontSize: '16px', lineHeight: 1.2 }}>{day.icon}</div> }}>
<div style={{ fontFamily: 'JetBrains Mono, monospace', marginTop: '2px' }}> <div style={{ fontSize: '10px', color: 'var(--text-muted)', marginBottom: '6px', fontWeight: '600' }}>FORECAST</div>
<span style={{ color: 'var(--accent-amber)' }}>{day.high}°</span> <div style={{ display: 'flex', gap: '4px' }}>
<span style={{ color: 'var(--text-muted)' }}>/</span> {w.daily.map((day, i) => (
<span style={{ color: 'var(--accent-blue)' }}>{day.low}°</span> <div key={i} style={{
</div> flex: 1,
{day.precipProb > 0 && ( textAlign: 'center',
<div style={{ color: 'var(--accent-blue)', fontSize: '9px', marginTop: '1px' }}> padding: '6px 2px',
💧{day.precipProb}% background: 'var(--bg-tertiary)',
borderRadius: '4px',
fontSize: '10px',
}}>
<div style={{ color: 'var(--text-muted)', fontWeight: '600', marginBottom: '2px' }}>{i === 0 ? 'Today' : day.date}</div>
<div style={{ fontSize: '16px', lineHeight: 1.2 }}>{day.icon}</div>
<div style={{ fontFamily: 'JetBrains Mono, monospace', marginTop: '2px' }}>
<span style={{ color: 'var(--accent-amber)' }}>{day.high}°</span>
<span style={{ color: 'var(--text-muted)' }}>/</span>
<span style={{ color: 'var(--accent-blue)' }}>{day.low}°</span>
</div> </div>
)} {day.precipProb > 0 && (
</div> <div style={{ color: 'var(--accent-blue)', fontSize: '9px', marginTop: '1px' }}>
))} 💧{day.precipProb}%
</div>
)}
</div>
))}
</div>
</div> </div>
</div> )}
)} </div>
</div> )}
)} </div>
</div> );
); })()}
})()} </div>
</div> )}
{/* DX Location */} {/* DX Location */}
<div className="panel" style={{ padding: '14px', flex: '0 0 auto' }}> {config.panels?.dxLocation?.visible !== false && (
<div style={{ fontSize: '14px', color: 'var(--accent-green)', fontWeight: '700', marginBottom: '10px' }}>🎯 DX - TARGET</div> <div className="panel" style={{ padding: '14px', flex: '0 0 auto' }}>
<div style={{ fontFamily: 'JetBrains Mono', fontSize: '14px' }}> <div style={{ fontSize: '14px', color: 'var(--accent-green)', fontWeight: '700', marginBottom: '10px' }}>🎯 DX - TARGET</div>
<div style={{ color: 'var(--accent-amber)', fontSize: '22px', fontWeight: '700', letterSpacing: '1px' }}>{dxGrid}</div> <div style={{ fontFamily: 'JetBrains Mono', fontSize: '14px' }}>
<div style={{ color: 'var(--text-secondary)', fontSize: '13px', marginTop: '4px' }}>{dxLocation.lat.toFixed(4)}°, {dxLocation.lon.toFixed(4)}°</div> <div style={{ color: 'var(--accent-amber)', fontSize: '22px', fontWeight: '700', letterSpacing: '1px' }}>{dxGrid}</div>
<div style={{ marginTop: '8px', fontSize: '13px' }}> <div style={{ color: 'var(--text-secondary)', fontSize: '13px', marginTop: '4px' }}>{dxLocation.lat.toFixed(4)}°, {dxLocation.lon.toFixed(4)}°</div>
<span style={{ color: 'var(--text-secondary)' }}> </span> <div style={{ marginTop: '8px', fontSize: '13px' }}>
<span style={{ color: 'var(--accent-amber)', fontWeight: '600' }}>{dxSunTimes.sunrise}</span> <span style={{ color: 'var(--text-secondary)' }}> </span>
<span style={{ color: 'var(--text-secondary)' }}> </span> <span style={{ color: 'var(--accent-amber)', fontWeight: '600' }}>{dxSunTimes.sunrise}</span>
<span style={{ color: 'var(--accent-purple)', fontWeight: '600' }}>{dxSunTimes.sunset}</span> <span style={{ color: 'var(--text-secondary)' }}> </span>
<span style={{ color: 'var(--accent-purple)', fontWeight: '600' }}>{dxSunTimes.sunset}</span>
</div>
</div> </div>
</div> </div>
</div> )}
{/* Solar Panel */} {/* Solar Panel */}
<SolarPanel solarIndices={solarIndices} /> {config.panels?.solar?.visible !== false && (
<SolarPanel solarIndices={solarIndices} />
)}
{/* VOACAP/Propagation Panel */} {/* VOACAP/Propagation Panel */}
<PropagationPanel {config.panels?.propagation?.visible !== false && (
propagation={propagation.data} <PropagationPanel
loading={propagation.loading} propagation={propagation.data}
bandConditions={bandConditions} loading={propagation.loading}
/> bandConditions={bandConditions}
/>
)}
</div> </div>
)}
{/* CENTER - MAP */} {/* CENTER - MAP */}
<div style={{ position: 'relative', borderRadius: '6px', overflow: 'hidden' }}> <div style={{ position: 'relative', borderRadius: '6px', overflow: 'hidden', width: '100%', height: '100%', minWidth: 0 }}>
<WorldMap <WorldMap
deLocation={config.location} deLocation={config.location}
dxLocation={dxLocation} dxLocation={dxLocation}
@ -852,71 +889,83 @@ const App = () => {
</div> </div>
{/* RIGHT SIDEBAR */} {/* RIGHT SIDEBAR */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', overflow: 'hidden' }}> {rightSidebarVisible && (
{/* DX Cluster - primary panel, takes most space */} <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', overflow: 'hidden' }}>
<div style={{ flex: '2 1 auto', minHeight: '180px', overflow: 'hidden' }}> {/* DX Cluster - primary panel, takes most space */}
<DXClusterPanel {config.panels?.dxCluster?.visible !== false && (
data={dxCluster.data} <div style={{ flex: `${config.panels.dxCluster.size || 2} 1 auto`, minHeight: '180px', overflow: 'hidden' }}>
loading={dxCluster.loading} <DXClusterPanel
totalSpots={dxCluster.totalSpots} data={dxCluster.data}
filters={dxFilters} loading={dxCluster.loading}
onFilterChange={setDxFilters} totalSpots={dxCluster.totalSpots}
onOpenFilters={() => setShowDXFilters(true)} filters={dxFilters}
onHoverSpot={setHoveredSpot} onFilterChange={setDxFilters}
hoveredSpot={hoveredSpot} onOpenFilters={() => setShowDXFilters(true)}
showOnMap={mapLayers.showDXPaths} onHoverSpot={setHoveredSpot}
onToggleMap={toggleDXPaths} hoveredSpot={hoveredSpot}
/> showOnMap={mapLayers.showDXPaths}
</div> onToggleMap={toggleDXPaths}
/>
{/* PSKReporter + WSJT-X - digital mode spots */} </div>
<div style={{ flex: '1 1 auto', minHeight: '140px', overflow: 'hidden' }}> )}
<PSKReporterPanel
callsign={config.callsign} {/* PSKReporter + WSJT-X - digital mode spots */}
showOnMap={mapLayers.showPSKReporter} {config.panels?.pskReporter?.visible !== false && (
onToggleMap={togglePSKReporter} <div style={{ flex: `${config.panels.pskReporter.size || 1} 1 auto`, minHeight: '140px', overflow: 'hidden' }}>
filters={pskFilters} <PSKReporterPanel
onOpenFilters={() => setShowPSKFilters(true)} callsign={config.callsign}
onShowOnMap={(report) => { showOnMap={mapLayers.showPSKReporter}
if (report.lat && report.lon) { onToggleMap={togglePSKReporter}
setDxLocation({ lat: report.lat, lon: report.lon, call: report.receiver || report.sender }); filters={pskFilters}
} onOpenFilters={() => setShowPSKFilters(true)}
}} onShowOnMap={(report) => {
wsjtxDecodes={wsjtx.decodes} if (report.lat && report.lon) {
wsjtxClients={wsjtx.clients} setDxLocation({ lat: report.lat, lon: report.lon, call: report.receiver || report.sender });
wsjtxQsos={wsjtx.qsos} }
wsjtxStats={wsjtx.stats} }}
wsjtxLoading={wsjtx.loading} wsjtxDecodes={wsjtx.decodes}
wsjtxEnabled={wsjtx.enabled} wsjtxClients={wsjtx.clients}
wsjtxPort={wsjtx.port} wsjtxQsos={wsjtx.qsos}
wsjtxRelayEnabled={wsjtx.relayEnabled} wsjtxStats={wsjtx.stats}
wsjtxRelayConnected={wsjtx.relayConnected} wsjtxLoading={wsjtx.loading}
wsjtxSessionId={wsjtx.sessionId} wsjtxEnabled={wsjtx.enabled}
showWSJTXOnMap={mapLayers.showWSJTX} wsjtxPort={wsjtx.port}
onToggleWSJTXMap={toggleWSJTX} wsjtxRelayEnabled={wsjtx.relayEnabled}
/> wsjtxRelayConnected={wsjtx.relayConnected}
</div> wsjtxSessionId={wsjtx.sessionId}
showWSJTXOnMap={mapLayers.showWSJTX}
{/* DXpeditions */} onToggleWSJTXMap={toggleWSJTX}
<div style={{ flex: '0 0 auto', minHeight: '70px', maxHeight: '100px', overflow: 'hidden' }}> />
<DXpeditionPanel data={dxpeditions.data} loading={dxpeditions.loading} /> </div>
</div> )}
{/* POTA */} {/* DXpeditions */}
<div style={{ flex: '0 0 auto', minHeight: '60px', maxHeight: '90px', overflow: 'hidden' }}> {config.panels?.dxpeditions?.visible !== false && (
<POTAPanel <div style={{ flex: `${config.panels.dxpeditions?.size || 1} 0 auto`, minHeight: '70px', maxHeight: '100px', overflow: 'hidden' }}>
data={potaSpots.data} <DXpeditionPanel data={dxpeditions.data} loading={dxpeditions.loading} />
loading={potaSpots.loading} </div>
showOnMap={mapLayers.showPOTA} )}
onToggleMap={togglePOTA}
/> {/* POTA */}
</div> {config.panels?.pota?.visible !== false && (
<div style={{ flex: `${config.panels.pota?.size || 1} 0 auto`, minHeight: '60px', maxHeight: '90px', overflow: 'hidden' }}>
{/* Contests - at bottom, compact */} <POTAPanel
<div style={{ flex: '0 0 auto', minHeight: '80px', maxHeight: '120px', overflow: 'hidden' }}> data={potaSpots.data}
<ContestPanel data={contests.data} loading={contests.loading} /> loading={potaSpots.loading}
showOnMap={mapLayers.showPOTA}
onToggleMap={togglePOTA}
/>
</div>
)}
{/* Contests - at bottom, compact */}
{config.panels?.contests?.visible !== false && (
<div style={{ flex: `${config.panels.contests?.size || 1} 0 auto`, minHeight: '80px', maxHeight: '120px', overflow: 'hidden' }}>
<ContestPanel data={contests.data} loading={contests.loading} />
</div>
)}
</div> </div>
</div> )}
</div> </div>
)} )}

@ -23,6 +23,19 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
const [layers, setLayers] = useState([]); const [layers, setLayers] = useState([]);
const [activeTab, setActiveTab] = useState('station'); const [activeTab, setActiveTab] = useState('station');
// Panel settings
const [panels, setPanels] = useState(config?.panels || {
deLocation: { visible: true, size: 1.0 },
dxLocation: { visible: true, size: 1.0 },
solar: { visible: true, size: 1.0 },
propagation: { visible: true, size: 1.0 },
dxCluster: { visible: true, size: 2.0 },
pskReporter: { visible: true, size: 1.0 },
dxpeditions: { visible: true, size: 1.0 },
pota: { visible: true, size: 1.0 },
contests: { visible: true, size: 1.0 }
});
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setCallsign(config.callsign || ''); setCallsign(config.callsign || '');
@ -33,6 +46,17 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
setLayout(config.layout || 'modern'); setLayout(config.layout || 'modern');
setTimezone(config.timezone || ''); setTimezone(config.timezone || '');
setDxClusterSource(config.dxClusterSource || 'dxspider-proxy'); setDxClusterSource(config.dxClusterSource || 'dxspider-proxy');
setPanels(config.panels || {
deLocation: { visible: true, size: 1.0 },
dxLocation: { visible: true, size: 1.0 },
solar: { visible: true, size: 1.0 },
propagation: { visible: true, size: 1.0 },
dxCluster: { visible: true, size: 2.0 },
pskReporter: { visible: true, size: 1.0 },
dxpeditions: { visible: true, size: 1.0 },
pota: { visible: true, size: 1.0 },
contests: { visible: true, size: 1.0 }
});
if (config.location?.lat && config.location?.lon) { if (config.location?.lat && config.location?.lon) {
setGridSquare(calculateGridSquare(config.location.lat, config.location.lon)); setGridSquare(calculateGridSquare(config.location.lat, config.location.lon));
} }
@ -145,6 +169,26 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
} }
}; };
const handleTogglePanel = (panelId) => {
setPanels(prev => ({
...prev,
[panelId]: {
...prev[panelId],
visible: !prev[panelId].visible
}
}));
};
const handlePanelSizeChange = (panelId, size) => {
setPanels(prev => ({
...prev,
[panelId]: {
...prev[panelId],
size: parseFloat(size)
}
}));
};
const handleSave = () => { const handleSave = () => {
onSave({ onSave({
...config, ...config,
@ -154,7 +198,8 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
theme, theme,
layout, layout,
timezone, timezone,
dxClusterSource dxClusterSource,
panels
}); });
onClose(); onClose();
}; };
@ -237,6 +282,23 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
> >
Station Station
</button> </button>
<button
onClick={() => setActiveTab('panels')}
style={{
flex: 1,
padding: '10px',
background: activeTab === 'panels' ? 'var(--accent-amber)' : 'transparent',
border: 'none',
borderRadius: '6px 6px 0 0',
color: activeTab === 'panels' ? '#000' : 'var(--text-secondary)',
fontSize: '13px',
cursor: 'pointer',
fontWeight: activeTab === 'panels' ? '700' : '400',
fontFamily: 'JetBrains Mono, monospace'
}}
>
Panels
</button>
<button <button
onClick={() => setActiveTab('layers')} onClick={() => setActiveTab('layers')}
style={{ style={{
@ -657,6 +719,153 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
</> </>
)} )}
{/* Panels Tab */}
{activeTab === 'panels' && (
<div>
<div style={{ marginBottom: '16px', fontSize: '12px', color: 'var(--text-muted)' }}>
Toggle panels on/off and adjust their relative sizes
</div>
{/* Left Sidebar Panels */}
<div style={{ marginBottom: '20px' }}>
<div style={{ fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '8px', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '1px' }}>
LEFT SIDEBAR
</div>
{[
{ id: 'deLocation', name: '📍 DE Location', description: 'Your station location and weather' },
{ id: 'dxLocation', name: '🎯 DX Target', description: 'Target location for DXing' },
{ id: 'solar', name: '☀️ Solar Indices', description: 'Sunspot numbers and solar flux' },
{ id: 'propagation', name: '📡 Propagation', description: 'Band conditions and forecasts' }
].map(panel => (
<div key={panel.id} style={{
background: 'var(--bg-tertiary)',
border: `1px solid ${panels[panel.id]?.visible !== false ? 'var(--accent-amber)' : 'var(--border-color)'}`,
borderRadius: '8px',
padding: '12px',
marginBottom: '8px'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
cursor: 'pointer',
flex: 1
}}>
<input
type="checkbox"
checked={panels[panel.id]?.visible !== false}
onChange={() => handleTogglePanel(panel.id)}
style={{
width: '18px',
height: '18px',
cursor: 'pointer'
}}
/>
<div>
<div style={{
color: panels[panel.id]?.visible !== false ? 'var(--accent-amber)' : 'var(--text-primary)',
fontSize: '13px',
fontWeight: '600',
fontFamily: 'JetBrains Mono, monospace'
}}>
{panel.name}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
{panel.description}
</div>
</div>
</label>
</div>
</div>
))}
</div>
{/* Right Sidebar Panels */}
<div>
<div style={{ fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '8px', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '1px' }}>
RIGHT SIDEBAR
</div>
{[
{ id: 'dxCluster', name: '📻 DX Cluster', description: 'Live DX spots from cluster', hasSize: true, defaultSize: 2.0 },
{ id: 'pskReporter', name: '📡 PSK Reporter', description: 'Digital mode spots and WSJT-X', hasSize: true, defaultSize: 1.0 },
{ id: 'dxpeditions', name: '🏝️ DXpeditions', description: 'Upcoming DXpeditions', hasSize: true, defaultSize: 1.0 },
{ id: 'pota', name: '🏕️ POTA', description: 'Parks on the Air activators', hasSize: true, defaultSize: 1.0 },
{ id: 'contests', name: '🏆 Contests', description: 'Upcoming and active contests', hasSize: true, defaultSize: 1.0 }
].map(panel => (
<div key={panel.id} style={{
background: 'var(--bg-tertiary)',
border: `1px solid ${panels[panel.id]?.visible !== false ? 'var(--accent-amber)' : 'var(--border-color)'}`,
borderRadius: '8px',
padding: '12px',
marginBottom: '8px'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
cursor: 'pointer',
flex: 1
}}>
<input
type="checkbox"
checked={panels[panel.id]?.visible !== false}
onChange={() => handleTogglePanel(panel.id)}
style={{
width: '18px',
height: '18px',
cursor: 'pointer'
}}
/>
<div>
<div style={{
color: panels[panel.id]?.visible !== false ? 'var(--accent-amber)' : 'var(--text-primary)',
fontSize: '13px',
fontWeight: '600',
fontFamily: 'JetBrains Mono, monospace'
}}>
{panel.name}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
{panel.description}
</div>
</div>
</label>
</div>
{panels[panel.id]?.visible !== false && panel.hasSize && (
<div style={{ paddingLeft: '28px', marginTop: '8px' }}>
<label style={{
display: 'block',
fontSize: '11px',
color: 'var(--text-muted)',
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Size: {panels[panel.id]?.size || panel.defaultSize}x
</label>
<input
type="range"
min="0.5"
max="3.0"
step="0.1"
value={panels[panel.id]?.size || panel.defaultSize}
onChange={(e) => handlePanelSizeChange(panel.id, e.target.value)}
style={{
width: '100%',
cursor: 'pointer'
}}
/>
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Map Layers Tab */} {/* Map Layers Tab */}
{activeTab === 'layers' && ( {activeTab === 'layers' && (
<div> <div>

@ -22,6 +22,19 @@ export const DEFAULT_CONFIG = {
showSatellites: true, showSatellites: true,
showPota: true, showPota: true,
showDxPaths: true, showDxPaths: true,
panels: {
// Left sidebar panels
deLocation: { visible: true, size: 1.0 },
dxLocation: { visible: true, size: 1.0 },
solar: { visible: true, size: 1.0 },
propagation: { visible: true, size: 1.0 },
// Right sidebar panels
dxCluster: { visible: true, size: 2.0 },
pskReporter: { visible: true, size: 1.0 },
dxpeditions: { visible: true, size: 1.0 },
pota: { visible: true, size: 1.0 },
contests: { visible: true, size: 1.0 }
},
refreshIntervals: { refreshIntervals: {
spaceWeather: 300000, // 5 minutes spaceWeather: 300000, // 5 minutes
bandConditions: 300000, // 5 minutes bandConditions: 300000, // 5 minutes
@ -83,6 +96,7 @@ export const loadConfig = () => {
// Ensure nested objects are properly merged // Ensure nested objects are properly merged
location: localConfig.location || config.location, location: localConfig.location || config.location,
defaultDX: localConfig.defaultDX || config.defaultDX, defaultDX: localConfig.defaultDX || config.defaultDX,
panels: { ...config.panels, ...localConfig.panels },
refreshIntervals: { ...config.refreshIntervals, ...localConfig.refreshIntervals } refreshIntervals: { ...config.refreshIntervals, ...localConfig.refreshIntervals }
}; };
} }
@ -92,7 +106,7 @@ export const loadConfig = () => {
// But only if they have real values (not N0CALL) // But only if they have real values (not N0CALL)
config = { config = {
...config, ...config,
callsign: (serverConfig.callsign && serverConfig.callsign !== 'N0CALL') callsign: (serverConfig.callsign && serverConfig.callsign !== 'N0CALL')
? serverConfig.callsign : config.callsign, ? serverConfig.callsign : config.callsign,
locator: serverConfig.locator || config.locator, locator: serverConfig.locator || config.locator,
location: { location: {
@ -110,7 +124,8 @@ export const loadConfig = () => {
use12Hour: serverConfig.timeFormat === '12', use12Hour: serverConfig.timeFormat === '12',
showSatellites: serverConfig.showSatellites ?? config.showSatellites, showSatellites: serverConfig.showSatellites ?? config.showSatellites,
showPota: serverConfig.showPota ?? config.showPota, showPota: serverConfig.showPota ?? config.showPota,
showDxPaths: serverConfig.showDxPaths ?? config.showDxPaths showDxPaths: serverConfig.showDxPaths ?? config.showDxPaths,
panels: { ...config.panels, ...serverConfig.panels }
}; };
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.