feat: complete dashboard redesign, proxy unification, and Windows compatibility fixes

This commit is contained in:
2026-04-02 12:13:37 +02:00
parent 8192388c5d
commit fec4e4635c
33 changed files with 3002 additions and 135 deletions

View File

@@ -11,6 +11,8 @@ export default function NavigationMap() {
const shipMarkerRef = useRef(null)
const trackSourceRef = useRef(false)
const [zoom, setZoom] = useState(11)
const [mapError, setMapError] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const api = getApi()
const snapshot = api.signalk.getSnapshot?.()
@@ -22,7 +24,9 @@ export default function NavigationMap() {
useEffect(() => {
if (map.current) return
map.current = new maplibregl.Map({
try {
setIsLoading(true)
map.current = new maplibregl.Map({
container: mapContainer.current,
style: {
version: 8,
@@ -177,9 +181,28 @@ export default function NavigationMap() {
})
map.current.on('zoom', () => setZoom(map.current.getZoom()))
map.current.on('load', () => {
setIsLoading(false)
setMapError(null)
})
map.current.on('error', (e) => {
console.error('Map error:', e)
setMapError('Map failed to load. Check your internet connection.')
setIsLoading(false)
})
return () => {
// Cleanup is handled by React
if (map.current) {
map.current.remove()
map.current = null
}
}
} catch (error) {
console.error('Failed to initialize map:', error)
setMapError(`Map initialization failed: ${error.message}`)
setIsLoading(false)
}
}, [])
@@ -191,11 +214,14 @@ export default function NavigationMap() {
const trackSource = map.current.getSource('track')
if (trackSource && trackSourceRef.current) {
const currentData = trackSource._data
if (currentData.geometry.coordinates.length > 500) {
currentData.geometry.coordinates.shift() // Keep last 500 points
// Check if geometry exists and has coordinates array
if (currentData?.geometry?.coordinates) {
if (currentData.geometry.coordinates.length > 500) {
currentData.geometry.coordinates.shift() // Keep last 500 points
}
currentData.geometry.coordinates.push([lon, lat])
trackSource.setData(currentData)
}
currentData.geometry.coordinates.push([lon, lat])
trackSource.setData(currentData)
}
map.current.getSource('ship').setData({
@@ -214,8 +240,34 @@ export default function NavigationMap() {
}
}, [lat, lon, heading, zoom])
// Show error state
if (mapError) {
return (
<div style={styles.container}>
<div style={styles.errorState}>
<div style={styles.errorIcon}></div>
<div style={styles.errorTitle}>Karte konnte nicht geladen werden</div>
<div style={styles.errorMessage}>{mapError}</div>
<button
className="primary"
onClick={() => window.location.reload()}
style={{ marginTop: 16 }}
>
Neu laden
</button>
</div>
</div>
)
}
return (
<div style={styles.container}>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
<div style={{ marginTop: 12, color: 'var(--muted)' }}>Karte wird geladen...</div>
</div>
)}
<div ref={mapContainer} style={styles.mapBox} />
{/* Map Controls */}
@@ -384,4 +436,51 @@ const styles = {
padding: '4px 8px',
zIndex: 500,
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'var(--surface)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
zIndex: 2000,
},
spinner: {
width: 40,
height: 40,
border: '4px solid var(--border)',
borderTop: '4px solid var(--accent)',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
},
errorState: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
padding: 32,
textAlign: 'center',
background: 'var(--surface)',
borderRadius: 'var(--radius)',
},
errorIcon: {
fontSize: 48,
marginBottom: 16,
},
errorTitle: {
fontSize: 18,
fontWeight: 600,
color: 'var(--text)',
marginBottom: 8,
},
errorMessage: {
fontSize: 14,
color: 'var(--muted)',
maxWidth: 400,
},
}

View File

@@ -0,0 +1,204 @@
import { useNMEA } from '../../hooks/useNMEA.js'
// Simple fallback map component without MapLibre (no external dependencies)
export default function SimpleNavigationMap() {
const { lat, lon, heading, sog, cog } = useNMEA()
const mapLat = lat ?? 54.32
const mapLon = lon ?? 10.22
// Simple marker rotation based on heading
const rotation = heading ?? cog ?? 0
return (
<div style={styles.container}>
{/* Info Panel */}
<div style={styles.infoPanel}>
<div style={styles.infoRow}>
<span style={styles.label}>Position</span>
<span style={styles.value}>
{lat != null ? lat.toFixed(5) : '--'}° N
</span>
</div>
<div style={styles.infoRow}>
<span style={styles.label}></span>
<span style={styles.value}>
{lon != null ? lon.toFixed(5) : '--'}° E
</span>
</div>
{heading != null && (
<div style={styles.infoRow}>
<span style={styles.label}>Heading</span>
<span style={styles.value}>{Math.round(heading)}°</span>
</div>
)}
{sog != null && (
<div style={styles.infoRow}>
<span style={styles.label}>Speed</span>
<span style={styles.value}>{(sog * 1.943844).toFixed(1)} kn</span>
</div>
)}
</div>
{/* Simple visual representation */}
<div style={styles.mapView}>
<div style={styles.compassRose}>
<div style={styles.northMarker}>N</div>
<div
style={{
...styles.shipMarker,
transform: `rotate(${rotation}deg)`
}}
>
</div>
</div>
<div style={styles.coords}>
{lat != null && lon != null ? (
<>
<div>{lat.toFixed(5)}°</div>
<div>{lon.toFixed(5)}°</div>
</>
) : (
<div style={{ color: 'var(--muted)' }}>No GPS signal</div>
)}
</div>
</div>
{/* Map notice */}
<div style={styles.notice}>
<div style={styles.noticeTitle}>🗺 Karten-Modus</div>
<div style={styles.noticeText}>
Interaktive Seekarte ist im Production Mode verfügbar.
<br />
Hier siehst du Position, Kurs und Geschwindigkeit.
</div>
<a
href="http://localhost:3000"
target="_blank"
rel="noreferrer"
style={styles.link}
>
SignalK Karte öffnen (Port 3000)
</a>
</div>
</div>
)
}
const styles = {
container: {
position: 'relative',
flex: 1,
background: 'var(--surface)',
borderRadius: 'var(--radius)',
border: '1px solid var(--border)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
minHeight: 400,
},
mapView: {
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 32,
padding: 32,
},
compassRose: {
position: 'relative',
width: 180,
height: 180,
border: '2px solid var(--accent)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'radial-gradient(circle, rgba(14, 165, 233, 0.05) 0%, transparent 70%)',
},
northMarker: {
position: 'absolute',
top: 8,
fontSize: 14,
fontWeight: 700,
color: 'var(--accent)',
},
shipMarker: {
fontSize: 48,
color: 'var(--accent)',
transition: 'transform 0.5s ease-out',
},
coords: {
fontFamily: 'var(--font-mono)',
fontSize: 16,
fontWeight: 600,
color: 'var(--text)',
textAlign: 'center',
lineHeight: 1.5,
},
infoPanel: {
background: 'var(--glass-bg)',
backdropFilter: 'blur(var(--glass-blur))',
WebkitBackdropFilter: 'blur(var(--glass-blur))',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: 'var(--radius-lg)',
padding: '12px 16px',
margin: 16,
display: 'flex',
flexDirection: 'column',
gap: 8,
},
infoRow: {
display: 'flex',
justifyContent: 'space-between',
gap: 16,
},
label: {
color: 'var(--muted)',
fontWeight: 600,
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: '0.05em',
},
value: {
fontFamily: 'var(--font-mono)',
fontWeight: 600,
color: 'var(--accent)',
fontSize: 13,
},
notice: {
background: 'var(--surface2)',
padding: 20,
borderTop: '1px solid var(--border)',
textAlign: 'center',
},
noticeTitle: {
fontSize: 14,
fontWeight: 600,
color: 'var(--text)',
marginBottom: 8,
},
noticeText: {
fontSize: 12,
color: 'var(--muted)',
lineHeight: 1.5,
marginBottom: 12,
},
link: {
display: 'inline-block',
fontSize: 12,
color: 'var(--accent)',
textDecoration: 'none',
fontWeight: 600,
padding: '6px 12px',
borderRadius: 'var(--radius)',
background: 'rgba(14, 165, 233, 0.1)',
border: '1px solid rgba(14, 165, 233, 0.3)',
transition: 'all 0.2s',
},
}