feat: import new reservoir data, add lake management scripts, and update overview UI components
continuous-integration/drone/push Build encountered an error

This commit is contained in:
David Fencl
2026-06-06 20:14:36 +02:00
parent cf05e844d8
commit a67a2247c3
55 changed files with 265428 additions and 441 deletions
+14 -10
View File
@@ -20,6 +20,7 @@ interface Lake {
inflow: number;
outflow: number;
volume: number;
maxVolume: number;
sparkline: number[];
}
@@ -122,21 +123,24 @@ const FavoritesOverview = ({ language }: Props) => {
{lake.name} {lake.river ? `- ${lake.river}` : ''}
</h3>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{t[language].kpi.level}</div>
<div style={{ fontSize: '2rem', fontWeight: 'bold' }}>{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)' }}>m n.m.</span></div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}>
<CircularProgress value={lake.capacity} size={70} strokeWidth={6} />
<div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{t[language].kpi.level}</div>
<div style={{ fontSize: '2rem', fontWeight: 'bold', display: 'flex', alignItems: 'baseline', gap: '0.25rem', lineHeight: '1.1' }}>
{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>m n.m.</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<CircularProgress value={lake.capacity} size={70} strokeWidth={6} />
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginTop: '0.25rem' }}>
{lake.storageDiff !== undefined && (
<div style={{ fontSize: '1.25rem', color: lake.storageDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold' }}>
<div style={{ fontSize: '1rem', color: lake.storageDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', whiteSpace: 'nowrap' }}>
{lake.storageDiff > 0 ? '+' : ''}{lake.storageDiff.toFixed(2)} m
</div>
)}
{lake.maxVolume > 0 && (
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
{lake.volume.toFixed(1)} / {lake.maxVolume.toFixed(1)} mil. m³
</div>
)}
</div>
</div>
</div>
+12 -12
View File
@@ -42,26 +42,26 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
<>
{/* CARD 1: WATER LEVEL */}
<div className="kpi-card">
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
{dict.level} {lakeName}
</div>
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, marginBottom: '0.5rem', whiteSpace: 'nowrap' }}>
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, whiteSpace: 'nowrap' }}>
{data.level.toFixed(2)} <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>m n. m.</span>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginTop: '0.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', background: 'rgba(0,0,0,0.15)', padding: '0.3rem 0.6rem', borderRadius: '6px' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', alignContent: 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>1D</span>
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff24h ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
{(data.levelDiff24h ?? 0) > 0 ? '+' : ''}{((data.levelDiff24h ?? 0) * 100).toFixed(1)} cm
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', background: 'rgba(0,0,0,0.15)', padding: '0.3rem 0.6rem', borderRadius: '6px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>7D</span>
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff7d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
{(data.levelDiff7d ?? 0) > 0 ? '+' : ''}{((data.levelDiff7d ?? 0) * 100).toFixed(1)} cm
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', background: 'rgba(0,0,0,0.15)', padding: '0.3rem 0.6rem', borderRadius: '6px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>30D</span>
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff30d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
{(data.levelDiff30d ?? 0) > 0 ? '+' : ''}{((data.levelDiff30d ?? 0) * 100).toFixed(1)} cm
@@ -72,13 +72,13 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
{/* CARD 2: FLOW */}
<div className="kpi-card">
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
{dict.flow}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', height: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', fontSize: '0.85rem' }}>
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#8b5cf6', marginRight: '6px', flexShrink: 0 }}></span>
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-green)', marginRight: '6px', flexShrink: 0 }}></span>
{dict.inflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)', marginLeft: '4px' }}>{data.inflow.toFixed(1)} m³/s</span>
</div>
{data.avgInflow24h !== undefined && (
@@ -87,7 +87,7 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
</div>
)}
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.25rem', whiteSpace: 'nowrap' }}>
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-orange)', marginRight: '2px', flexShrink: 0 }}></span>
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-red)', marginRight: '2px', flexShrink: 0 }}></span>
{dict.outflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{data.outflow.toFixed(1)} m³/s</span>
{flowDiff > 0 ? <FiArrowUp color="var(--color-green)" /> : flowDiff < 0 ? <FiArrowDown color="var(--color-red)" /> : null}
</div>
@@ -122,7 +122,7 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
{/* CARD 3: CAPACITY */}
<div className="kpi-card">
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.4rem', position: 'relative' }}>
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.4rem', position: 'relative' }}>
{dict.fullness}
<span
onClick={() => setShowTooltip(!showTooltip)}
@@ -155,7 +155,7 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
</div>
)}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '0.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', flex: 1, minWidth: 0, paddingRight: '0.5rem' }}>
<div style={{ fontSize: '1.7rem', fontWeight: 'bold', lineHeight: 1, marginBottom: '0.5rem', whiteSpace: 'nowrap', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)' }}>
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)} m` : `${data.storageDiff.toFixed(2)} m`) : (data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A')}
+8 -8
View File
@@ -59,8 +59,8 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
let unit = '';
let color = '';
if (entry.dataKey === 'level') { labelStr = dict.level; unit = 'm n. m.'; color = 'var(--color-cyan)'; }
else if (entry.dataKey === 'outflow') { labelStr = dict.outflow; unit = 'm³/s'; color = 'var(--color-orange)'; }
else if (entry.dataKey === 'inflow') { labelStr = dict.inflow; unit = 'm³/s'; color = '#8b5cf6'; }
else if (entry.dataKey === 'outflow') { labelStr = dict.outflow; unit = 'm³/s'; color = 'var(--color-red)'; }
else if (entry.dataKey === 'inflow') { labelStr = dict.inflow; unit = 'm³/s'; color = 'var(--color-green)'; }
else if (entry.dataKey === 'temperature') { labelStr = language === 'cs' ? 'Teplota' : 'Temperature'; unit = '°C'; color = 'var(--color-red)'; }
else if (entry.dataKey === 'precipitation') { labelStr = language === 'cs' ? 'Srážky' : 'Precipitation'; unit = 'mm'; color = 'var(--color-cyan)'; }
@@ -306,7 +306,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
{limits && limits.map((limit, idx) => {
const diff = latestData.level - limit.level;
if (diff < 1.0) {
if (diff < 0.3) {
const isBelow = diff < 0;
return (
<div key={idx} style={{ padding: '1rem', borderRadius: '8px', backgroundColor: isBelow ? 'rgba(248, 113, 113, 0.1)' : 'rgba(245, 158, 11, 0.1)', border: `1px solid ${isBelow ? 'var(--color-red)' : '#f59e0b'}`, color: isBelow ? 'var(--color-red)' : '#f59e0b', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
@@ -356,13 +356,13 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
{/* Data Series */}
{limits && limits.map((limit, idx) => (
<ReferenceLine key={idx} yAxisId="left" y={limit.level} stroke={limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b'} strokeDasharray="3 3" label={{ position: 'insideBottomRight', value: language === 'cs' ? limit.labelCs : limit.labelEn, fill: limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b', fontSize: 12 }} />
<ReferenceLine key={idx} yAxisId="left" y={limit.level} stroke={limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b'} strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `${limit.labelCs} (${limit.level.toFixed(2)} m n. m.)` : `${limit.labelEn} (${limit.level.toFixed(2)} m a.s.l.)`, fill: limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b', fontSize: 12 }} />
))}
{staticConfig && staticConfig.maxLevel && (
<ReferenceLine yAxisId="left" y={staticConfig.maxLevel} stroke="var(--color-orange)" strokeDasharray="3 3" label={{ position: 'insideTopLeft', value: language === 'cs' ? `Maximální retenční hladina (${staticConfig.maxLevel.toFixed(2)})` : `Max retention level (${staticConfig.maxLevel.toFixed(2)})`, fill: 'var(--color-orange)', fontSize: 12 }} />
{staticConfig?.maxLevel && (
<ReferenceLine yAxisId="left" y={staticConfig.maxLevel} stroke="var(--color-orange)" strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `Maximální retenční hladina (${staticConfig.maxLevel.toFixed(2)} m n. m.)` : `Max retention level (${staticConfig.maxLevel.toFixed(2)} m a.s.l.)`, fill: 'var(--color-orange)', fontSize: 12 }} />
)}
{staticConfig && staticConfig.storageLevel && (
<ReferenceLine yAxisId="left" y={staticConfig.storageLevel} stroke="#a855f7" strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `Hladina zásobního prostoru (${staticConfig.storageLevel.toFixed(2)})` : `Storage space level (${staticConfig.storageLevel.toFixed(2)})`, fill: '#a855f7', fontSize: 12 }} />
{staticConfig?.storageLevel && (
<ReferenceLine yAxisId="left" y={staticConfig.storageLevel} stroke="#a855f7" strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `Hladina zásobního prostoru (${staticConfig.storageLevel.toFixed(2)} m n. m.)` : `Storage space level (${staticConfig.storageLevel.toFixed(2)} m a.s.l.)`, fill: '#a855f7', fontSize: 12 }} />
)}
<Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" isAnimationActive={animate} />
<Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-red)" strokeWidth={2} dot={false} isAnimationActive={animate} />
+14 -15
View File
@@ -19,6 +19,7 @@ interface Lake {
inflow: number;
outflow: number;
volume: number;
maxVolume: number;
sparkline: number[];
}
@@ -73,26 +74,24 @@ const LakeCard = ({ lake, language, isFav, onToggleFav }: { lake: Lake, language
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0, paddingRight: '2rem' }}>{lake.name} {lake.river ? `- ${lake.river}` : ''}</h3>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<div style={{ width: '40px', height: '60px', backgroundColor: 'rgba(255,255,255,0.05)', position: 'relative', borderRadius: '4px', overflow: 'hidden' }}>
<div style={{ position: 'absolute', bottom: 0, left: 0, width: '100%', height: `${lake.capacity}%`, backgroundColor: 'var(--color-cyan)', opacity: 0.3 }}></div>
<div style={{ position: 'absolute', bottom: `${lake.capacity}%`, left: 0, width: '100%', height: '2px', backgroundColor: 'var(--color-cyan)' }}></div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}>
<CircularProgress value={lake.capacity} size={70} strokeWidth={6} />
<div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Water level</div>
<div style={{ fontSize: '2rem', fontWeight: 'bold', display: 'flex', alignItems: 'baseline', gap: '0.25rem', lineHeight: '1.1' }}>
{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>m n.m.</span>
</div>
<div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Water level</div>
<div style={{ fontSize: '2rem', fontWeight: 'bold' }}>{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)' }}>m n.m.</span></div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<CircularProgress value={lake.capacity} size={70} strokeWidth={6} />
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginTop: '0.25rem' }}>
{lake.storageDiff !== undefined && (
<div style={{ fontSize: '1.25rem', color: lake.storageDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold' }}>
<div style={{ fontSize: '1rem', color: lake.storageDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', whiteSpace: 'nowrap' }}>
{lake.storageDiff > 0 ? '+' : ''}{lake.storageDiff.toFixed(2)} m
</div>
)}
{lake.maxVolume > 0 && (
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
{lake.volume.toFixed(1)} / {lake.maxVolume.toFixed(1)} mil. m³
</div>
)}
</div>
</div>
</div>
+2 -2
View File
@@ -97,7 +97,7 @@ export const WeatherWidget = ({ lat, lng, language, sensorTemp, windUnit = 'kmh'
return (
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>{dict.title}</div>
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>{dict.title}</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '1rem', alignItems: 'center' }}>
@@ -113,7 +113,7 @@ export const WeatherWidget = ({ lat, lng, language, sensorTemp, windUnit = 'kmh'
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', lineHeight: 1.1, color: 'var(--text-main)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', lineHeight: 1.1, color: 'var(--text-main)', whiteSpace: 'nowrap' }}>
{data.windSpeed.toFixed(1)} <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'} {getCompassDirection(data.windDir, language)}</span>
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginTop: '4px', whiteSpace: 'nowrap' }}>
+64 -1
View File
@@ -16,12 +16,75 @@ export const NAVIGATION_LIMITS: Record<string, NavigationLimit[]> = {
}
],
// Slapy
'VLSL|1': [
'VLSL|2': [
{
level: 266.50,
labelCs: 'Minimální hladina pro převoz lodí Slapy',
labelEn: 'Minimum level for Slapy boat transport',
type: 'danger'
}
],
// Lipno 1
'VLL1|1': [
{
level: 719.60,
labelCs: 'Ukončení značení plavební dráhy',
labelEn: 'End of navigation channel marking',
type: 'danger'
}
],
// Hněvkovice
'VLHN|1': [
{
level: 368.90,
labelCs: 'Minimální plavební hladina',
labelEn: 'Minimum navigation level',
type: 'danger'
}
],
// Hracholusky
'MZHR|3': [
{
level: 351.10,
labelCs: 'Zkrácení zaručené plavební dráhy',
labelEn: 'Shortened guaranteed navigation channel',
type: 'warning'
}
],
// Kamýk
'VLKA|2': [
{
level: 283.60,
labelCs: 'Minimální plavební hladina',
labelEn: 'Minimum navigation level',
type: 'danger'
}
],
// Vrané
'VLVE|2': [
{
level: 199.30,
labelCs: 'Minimální plavební hladina',
labelEn: 'Minimum navigation level',
type: 'danger'
}
],
// Štěchovice
'VLST|2': [
{
level: 217.20,
labelCs: 'Minimální plavební hladina',
labelEn: 'Minimum navigation level',
type: 'danger'
}
],
// Kořensko
'VLKO|1': [
{
level: 352.00,
labelCs: 'Minimální plavební hladina',
labelEn: 'Minimum navigation level',
type: 'danger'
}
]
};