This commit is contained in:
David Fencl
2026-06-09 20:45:08 +02:00
parent c4cad149ea
commit c8fe97078d
56 changed files with 50749 additions and 48947 deletions
+540 -456
View File
File diff suppressed because it is too large Load Diff
+477 -391
View File
File diff suppressed because it is too large Load Diff
+522 -436
View File
File diff suppressed because it is too large Load Diff
+996 -978
View File
File diff suppressed because it is too large Load Diff
+1124 -1106
View File
File diff suppressed because it is too large Load Diff
+1122 -1104
View File
File diff suppressed because it is too large Load Diff
+1119 -1101
View File
File diff suppressed because it is too large Load Diff
+947 -947
View File
File diff suppressed because it is too large Load Diff
+1152 -1134
View File
File diff suppressed because it is too large Load Diff
+1121 -1103
View File
File diff suppressed because it is too large Load Diff
+1125 -1107
View File
File diff suppressed because it is too large Load Diff
+1011 -1011
View File
File diff suppressed because it is too large Load Diff
+1006 -1006
View File
File diff suppressed because it is too large Load Diff
+1139 -1121
View File
File diff suppressed because it is too large Load Diff
+1160 -1142
View File
File diff suppressed because it is too large Load Diff
+512 -428
View File
File diff suppressed because it is too large Load Diff
+454 -381
View File
File diff suppressed because it is too large Load Diff
+1135 -1117
View File
File diff suppressed because it is too large Load Diff
+1177 -1159
View File
File diff suppressed because it is too large Load Diff
+1174 -1156
View File
File diff suppressed because it is too large Load Diff
+1139 -1112
View File
File diff suppressed because it is too large Load Diff
+494 -408
View File
File diff suppressed because it is too large Load Diff
+535 -451
View File
File diff suppressed because it is too large Load Diff
+1030 -1012
View File
File diff suppressed because it is too large Load Diff
+1131 -1113
View File
File diff suppressed because it is too large Load Diff
+515 -429
View File
File diff suppressed because it is too large Load Diff
+454 -379
View File
File diff suppressed because it is too large Load Diff
+1128 -1110
View File
File diff suppressed because it is too large Load Diff
+516 -430
View File
File diff suppressed because it is too large Load Diff
+521 -435
View File
File diff suppressed because it is too large Load Diff
+1107 -1089
View File
File diff suppressed because it is too large Load Diff
+1118 -1100
View File
File diff suppressed because it is too large Load Diff
+1134 -1116
View File
File diff suppressed because it is too large Load Diff
+1068 -1050
View File
File diff suppressed because it is too large Load Diff
+1102 -1084
View File
File diff suppressed because it is too large Load Diff
+1133 -1106
View File
File diff suppressed because it is too large Load Diff
+1177 -1159
View File
File diff suppressed because it is too large Load Diff
+1156 -1129
View File
File diff suppressed because it is too large Load Diff
+1126 -1108
View File
File diff suppressed because it is too large Load Diff
+1183 -1165
View File
File diff suppressed because it is too large Load Diff
+464 -391
View File
File diff suppressed because it is too large Load Diff
+498 -412
View File
File diff suppressed because it is too large Load Diff
+1152 -1134
View File
File diff suppressed because it is too large Load Diff
+1096 -1078
View File
File diff suppressed because it is too large Load Diff
+1147 -1138
View File
File diff suppressed because it is too large Load Diff
+1104 -1086
View File
File diff suppressed because it is too large Load Diff
+1159 -1141
View File
File diff suppressed because it is too large Load Diff
+1123 -1123
View File
File diff suppressed because it is too large Load Diff
+1179 -1179
View File
File diff suppressed because it is too large Load Diff
+1139 -1130
View File
File diff suppressed because it is too large Load Diff
+518 -434
View File
File diff suppressed because it is too large Load Diff
+1123 -1105
View File
File diff suppressed because it is too large Load Diff
+1126 -1108
View File
File diff suppressed because it is too large Load Diff
+97 -97
View File
@@ -35,9 +35,9 @@
"name": "Lipno II",
"river": "Vltava",
"priority": true,
"level": "561.07",
"level": "561.06",
"capacity": 64.4,
"storageDiff": -1.63,
"storageDiff": -1.64,
"inflow": "11.2",
"outflow": "0.0",
"volume": 1.03,
@@ -46,8 +46,6 @@
"lat": 48.625,
"lng": 14.318,
"sparkline": [
561.14,
561.13,
561.12,
561.12,
561.11,
@@ -57,7 +55,9 @@
561.09,
561.08,
561.07,
561.07
561.07,
561.06,
561.06
],
"type": "lake"
},
@@ -77,8 +77,6 @@
"lat": 49.183,
"lng": 14.444,
"sparkline": [
369.52,
369.51,
369.51,
369.52,
369.52,
@@ -88,6 +86,8 @@
369.51,
369.51,
369.51,
369.51,
369.51,
369.51
],
"type": "lake"
@@ -108,7 +108,6 @@
"lat": 49.255,
"lng": 14.398,
"sparkline": [
352.56,
352.55,
352.55,
352.55,
@@ -119,6 +118,7 @@
352.54,
352.54,
352.54,
352.54,
352.54
],
"type": "lake"
@@ -128,11 +128,11 @@
"name": "Orlík",
"river": "Vltava",
"priority": true,
"level": "345.20",
"level": "345.19",
"capacity": 72.7,
"storageDiff": -4.7,
"storageDiff": -4.71,
"inflow": "27.6",
"outflow": "56.5",
"outflow": "73.6",
"volume": 520.93,
"maxVolume": 716.5,
"navigationForbidden": false,
@@ -142,7 +142,6 @@
345.19,
345.19,
345.19,
345.19,
345.2,
345.21,
345.21,
@@ -150,7 +149,8 @@
345.21,
345.21,
345.2,
345.2
345.2,
345.19
],
"type": "lake"
},
@@ -159,11 +159,11 @@
"name": "Slapy",
"river": "Vltava",
"priority": true,
"level": "269.76",
"level": "269.74",
"capacity": 96.6,
"storageDiff": -0.84,
"storageDiff": -0.86,
"inflow": "33.8",
"outflow": "257.4",
"outflow": "293.6",
"volume": 260.04,
"maxVolume": 269.3,
"navigationForbidden": false,
@@ -180,8 +180,8 @@
269.79,
269.79,
269.79,
269.79,
269.76
269.76,
269.74
],
"type": "lake"
},
@@ -190,18 +190,17 @@
"name": "Štěchovice",
"river": "Vltava",
"priority": true,
"level": "216.32",
"level": "216.58",
"capacity": 67.9,
"storageDiff": -3.08,
"storageDiff": -2.82,
"inflow": "49.2",
"outflow": "46.7",
"outflow": "60.8",
"volume": 7.6,
"maxVolume": 11.2,
"navigationForbidden": false,
"lat": 49.845,
"lng": 14.412,
"sparkline": [
216.51,
216.49,
216.49,
216.46,
@@ -212,7 +211,8 @@
216.39,
216.37,
216.35,
216.32
216.32,
216.58
],
"type": "lake"
},
@@ -256,7 +256,7 @@
"capacity": 56.4,
"storageDiff": -1.35,
"inflow": "1.0",
"outflow": "0.0",
"outflow": "2.5",
"volume": 31.99,
"maxVolume": 56.7,
"navigationForbidden": false,
@@ -267,13 +267,13 @@
352.75,
352.75,
352.75,
352.75,
352.75,
352.76,
352.75,
352.75,
352.75,
352.75,
352.75,
352.75,
352.75
],
"type": "lake"
@@ -283,9 +283,9 @@
"name": "Švihov (Želivka)",
"river": "",
"priority": true,
"level": "375.11",
"level": "375.10",
"capacity": 90.2,
"storageDiff": -1.89,
"storageDiff": -1.9,
"inflow": "0.9",
"outflow": "0.0",
"volume": 240.42,
@@ -305,7 +305,7 @@
375.11,
375.11,
375.11,
375.11
375.1
],
"type": "lake"
},
@@ -314,19 +314,17 @@
"name": "Kamýk",
"river": "",
"priority": false,
"level": "282.96",
"level": "283.02",
"capacity": 77.3,
"storageDiff": -1.64,
"storageDiff": -1.58,
"inflow": "26.3",
"outflow": "39.4",
"outflow": "39.5",
"volume": 9.89,
"maxVolume": 12.8,
"navigationForbidden": false,
"lat": 49.638,
"lng": 14.258,
"sparkline": [
282.97,
282.98,
282.98,
282.97,
282.98,
@@ -336,7 +334,9 @@
282.97,
282.97,
282.97,
282.96
282.96,
282.95,
283.02
],
"type": "lake"
},
@@ -345,9 +345,9 @@
"name": "Vrané",
"river": "",
"priority": false,
"level": "199.33",
"level": "199.34",
"capacity": 82.4,
"storageDiff": -0.77,
"storageDiff": -0.76,
"inflow": "47.8",
"outflow": "39.5",
"volume": 9.15,
@@ -356,8 +356,6 @@
"lat": 49.939,
"lng": 14.391,
"sparkline": [
199.33,
199.32,
199.32,
199.32,
199.32,
@@ -367,7 +365,9 @@
199.3,
199.3,
199.32,
199.33
199.33,
199.33,
199.34
],
"type": "lake"
},
@@ -380,7 +380,7 @@
"capacity": 42.6,
"storageDiff": -1.08,
"inflow": "0.5",
"outflow": "0.6",
"outflow": "0.7",
"volume": 2.43,
"maxVolume": 5.7,
"navigationForbidden": true,
@@ -449,8 +449,6 @@
"lat": 50.063,
"lng": 13.931,
"sparkline": [
292.88,
292.88,
292.88,
0,
292.88,
@@ -460,6 +458,8 @@
292.88,
292.88,
292.88,
292.88,
292.88,
292.88
],
"type": "lake"
@@ -511,8 +511,6 @@
"lat": 49.715,
"lng": 13.364,
"sparkline": [
313.43,
313.43,
313.43,
0,
313.42,
@@ -522,6 +520,8 @@
313.43,
0,
313.43,
313.42,
313.43,
0
],
"type": "lake"
@@ -566,15 +566,13 @@
"capacity": 88.4,
"storageDiff": -0.3,
"inflow": "0.6",
"outflow": "0.0",
"outflow": "0.3",
"volume": 1.68,
"maxVolume": 1.9,
"navigationForbidden": false,
"lat": 49.507,
"lng": 15.263,
"sparkline": [
447.09,
447.09,
447.09,
447.09,
447.09,
@@ -584,6 +582,8 @@
447.1,
447.1,
447.1,
447.1,
447.1,
447.1
],
"type": "lake"
@@ -597,7 +597,7 @@
"capacity": 100,
"storageDiff": -1.47,
"inflow": "0.3",
"outflow": "0.0",
"outflow": "0.5",
"volume": 2.91,
"maxVolume": 2.3,
"navigationForbidden": true,
@@ -605,8 +605,8 @@
"lng": 12.639,
"sparkline": [
530.64,
530.64,
530.64,
530.63,
530.63,
530.63,
530.63,
530.63,
@@ -717,9 +717,9 @@
"name": "Obecnice",
"river": "",
"priority": false,
"level": "563.65",
"level": "0.00",
"capacity": 76.7,
"storageDiff": -0.9,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "0.0",
"volume": 0.46,
@@ -739,7 +739,7 @@
563.65,
563.65,
563.65,
563.65
0
],
"type": "lake"
},
@@ -748,9 +748,9 @@
"name": "Strž",
"river": "",
"priority": false,
"level": "588.39",
"level": "0.00",
"capacity": 32,
"storageDiff": -0.21,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "0.1",
"volume": 0.32,
@@ -759,9 +759,6 @@
"lat": 49.791,
"lng": 14.004,
"sparkline": [
588.38,
588.38,
588.38,
588.38,
0,
588.39,
@@ -770,7 +767,10 @@
588.39,
588.39,
0,
588.39
588.39,
588.39,
588.39,
0
],
"type": "lake"
},
@@ -852,8 +852,6 @@
"lat": 48.784,
"lng": 14.735,
"sparkline": [
534.71,
534.71,
534.72,
0,
534.72,
@@ -863,6 +861,8 @@
534.72,
0,
534.72,
534.72,
534.72,
0
],
"type": "lake"
@@ -883,9 +883,6 @@
"lat": 49.575,
"lng": 15.952,
"sparkline": [
580.53,
580.53,
580.53,
0,
0,
580.53,
@@ -894,6 +891,9 @@
580.53,
580.53,
580.53,
580.53,
580.53,
580.53,
580.53
],
"type": "lake"
@@ -1217,15 +1217,13 @@
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "54.7",
"outflow": "54.2",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 50.0294,
"lng": 14.3986,
"sparkline": [
46,
47,
47,
47,
46,
@@ -1235,6 +1233,8 @@
47,
46,
46,
46,
46,
46
],
"type": "river"
@@ -1244,18 +1244,17 @@
"name": "České Budějovice",
"river": "Vltava",
"priority": false,
"level": "103.00",
"level": "106.00",
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "6.9",
"outflow": "6.7",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 48.9712,
"lng": 14.4714,
"sparkline": [
99,
99,
98,
98,
@@ -1266,7 +1265,8 @@
97,
98,
102,
103
103,
106
],
"type": "river"
},
@@ -1275,19 +1275,17 @@
"name": "Beroun",
"river": "Berounka",
"priority": false,
"level": "94.00",
"level": "95.00",
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "7.9",
"outflow": "8.4",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 49.9642,
"lng": 14.0792,
"sparkline": [
100,
99,
98,
97,
96,
@@ -1297,7 +1295,9 @@
93,
93,
93,
94
94,
95,
95
],
"type": "river"
},
@@ -1310,7 +1310,7 @@
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "3.8",
"outflow": "3.7",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
@@ -1319,7 +1319,7 @@
"sparkline": [
43,
43,
43,
42,
42,
42,
42,
@@ -1337,19 +1337,17 @@
"name": "Písek",
"river": "Otava",
"priority": false,
"level": "42.00",
"level": "45.00",
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "4.7",
"outflow": "5.1",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 49.3083,
"lng": 14.1436,
"sparkline": [
44,
45,
46,
44,
43,
@@ -1359,7 +1357,9 @@
44,
43,
43,
42
42,
42,
45
],
"type": "river"
},
@@ -1399,19 +1399,17 @@
"name": "Bechyně",
"river": "Lužnice",
"priority": false,
"level": "82.00",
"level": "83.00",
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "2.7",
"outflow": "2.8",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 49.2931,
"lng": 14.4758,
"sparkline": [
83,
81,
81,
84,
86,
@@ -1421,7 +1419,9 @@
83,
86,
85,
82
82,
80,
83
],
"type": "river"
},
@@ -1450,7 +1450,7 @@
50,
50,
50,
50,
51,
51,
51
],
@@ -1481,8 +1481,8 @@
67,
67,
67,
67,
67,
66,
66,
66
],
"type": "river"
@@ -1558,15 +1558,13 @@
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "5.8",
"outflow": "5.7",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 50.0436,
"lng": 13.9189,
"sparkline": [
150,
150,
150,
150,
150,
@@ -1576,6 +1574,8 @@
148,
148,
148,
147,
147,
147
],
"type": "river"
@@ -1596,8 +1596,6 @@
"lat": 49.7731,
"lng": 13.3986,
"sparkline": [
96,
95,
94,
93,
93,
@@ -1607,6 +1605,8 @@
90,
89,
89,
89,
89,
89
],
"type": "river"
+6 -4
View File
@@ -25,9 +25,10 @@ async function backfill() {
try {
const lat = lake.coords[0];
const lon = lake.coords[1];
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&past_days=7&hourly=temperature_2m,precipitation&timezone=GMT`;
// Fetch maximum past days supported by the forecast API (92 days)
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&past_days=92&hourly=temperature_2m,precipitation&timezone=GMT`;
const res = await axios.get(url, { timeout: 10000 });
const res = await axios.get(url, { timeout: 15000 });
const hourly = res.data.hourly;
// Build lookup map for O(1) matching: '2026-06-02T04:00' -> { temp, precip }
@@ -43,9 +44,10 @@ async function backfill() {
let updatedCount = 0;
for (const record of data) {
// record.timestamp is like "2026-06-02T04:00:00.000Z"
// record.timestamp is like "2026-06-02T04:10:00.000Z"
// Open-Meteo time is like "2026-06-02T04:00"
const hourKey = record.timestamp.substring(0, 16); // Extract up to minutes
// Convert to hourly key to match weatherMap
const hourKey = record.timestamp.substring(0, 13) + ':00';
if (weatherMap.has(hourKey)) {
const w = weatherMap.get(hourKey);
+38 -18
View File
@@ -200,15 +200,27 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
// Outlier/sensor glitch detection
if (level > 0) {
let isGlitch = false;
if (staticConfig && staticConfig.minLevel && staticConfig.maxLevel) {
const minAllowed = staticConfig.minLevel - 5;
const maxAllowed = staticConfig.maxLevel + 5;
if (level < minAllowed || level > maxAllowed) {
// Glitch detected, fallback to last known valid level
level = lastValidLevel !== null ? lastValidLevel : (staticConfig.minLevel + staticConfig.maxLevel) / 2;
} else {
lastValidLevel = level;
isGlitch = true;
}
}
// Check rate of change: sudden spikes/drops
if (!isGlitch && lastValidLevel !== null) {
const isRiver = staticConfig?.type === 'river';
const maxAllowedDelta = isRiver ? 50 : 0.5; // 50 cm for rivers, 0.5 m for reservoirs
if (Math.abs(level - lastValidLevel) > maxAllowedDelta) {
isGlitch = true;
}
}
if (isGlitch) {
// Glitch detected, fallback to last known valid level
level = lastValidLevel !== null ? lastValidLevel : (staticConfig && staticConfig.minLevel && staticConfig.maxLevel ? (staticConfig.minLevel + staticConfig.maxLevel) / 2 : level);
} else {
lastValidLevel = level;
}
@@ -394,15 +406,29 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
e.preventDefault();
const isTouchEvent = 'touches' in e;
const currentDomain = axis === 'left'
? (leftCustomDomain || getDefaultLeftDomain())
: (rightCustomDomain || getDefaultRightDomain());
// Calculate the center based on the middle of the actual data values to keep the graph line centered
let dataCenter = (currentDomain[1] + currentDomain[0]) / 2;
if (axis === 'left') {
const levels = chartData.map(d => d.level).filter(v => v !== null && v !== undefined && !isNaN(v));
if (levels.length > 0) {
dataCenter = (Math.min(...levels) + Math.max(...levels)) / 2;
}
} else {
const flows = chartData.flatMap(d => [d.outflow, d.inflow]).filter(v => v !== null && v !== undefined && !isNaN(v));
if (flows.length > 0) {
dataCenter = (Math.min(...flows) + Math.max(...flows)) / 2;
}
}
if (isTouchEvent && e.touches.length === 2) {
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const dist = Math.abs(touch1.clientY - touch2.clientY);
const currentDomain = axis === 'left'
? (leftCustomDomain || getDefaultLeftDomain())
: (rightCustomDomain || getDefaultRightDomain());
const onTouchMove = (moveEvent: TouchEvent) => {
if (moveEvent.touches.length === 2) {
moveEvent.preventDefault(); // Stop native page zooming
@@ -411,10 +437,9 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
const currentDist = Math.abs(mTouch1.clientY - mTouch2.clientY);
if (currentDist > 5) {
const factor = dist / currentDist;
const center = (currentDomain[0] + currentDomain[1]) / 2;
const range = currentDomain[1] - currentDomain[0];
const newMin = center - (range * factor) / 2;
const newMax = center + (range * factor) / 2;
const newMin = dataCenter - (range * factor) / 2;
const newMax = dataCenter + (range * factor) / 2;
if (axis === 'left') {
setLeftCustomDomain([newMin, newMax]);
@@ -436,12 +461,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
}
const startY = isTouchEvent ? e.touches[0].clientY : e.clientY;
const currentDomain = axis === 'left'
? (leftCustomDomain || getDefaultLeftDomain())
: (rightCustomDomain || getDefaultRightDomain());
const initialRange = currentDomain[1] - currentDomain[0];
const center = (currentDomain[1] + currentDomain[0]) / 2;
const onMove = (moveEvent: MouseEvent | TouchEvent) => {
if ('touches' in moveEvent) {
@@ -451,8 +471,8 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
const deltaY = clientY - startY;
const factor = Math.pow(2.5, deltaY / 150);
const newMin = center - (initialRange * factor) / 2;
const newMax = center + (initialRange * factor) / 2;
const newMin = dataCenter - (initialRange * factor) / 2;
const newMax = dataCenter + (initialRange * factor) / 2;
if (axis === 'left') {
setLeftCustomDomain([newMin, newMax]);