feat: add automatic data polling, conditional search visibility, and extended scraper functionality for monthly lake records

This commit is contained in:
David Fencl
2026-06-06 12:34:20 +02:00
parent dbb22e7972
commit db1aadcc8d
18 changed files with 2731 additions and 152 deletions
+46 -1
View File
@@ -7157,10 +7157,55 @@
{ {
"timestamp": "2026-06-06T09:30:00.000Z", "timestamp": "2026-06-06T09:30:00.000Z",
"level": 467.74, "level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 17.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 17.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 467.74,
"flow": 0, "flow": 0,
"inflow": 2.24, "inflow": 2.24,
"volume": 26.53, "volume": 26.53,
"temperature": 18.8, "temperature": 19.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 19.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 19.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:20:00.000Z",
"level": 467.74,
"flow": 0,
"inflow": 2.24,
"volume": 26.53,
"temperature": 19.9,
"precipitation": 0 "precipitation": 0
} }
] ]
+46 -1
View File
@@ -7157,10 +7157,55 @@
{ {
"timestamp": "2026-06-06T09:30:00.000Z", "timestamp": "2026-06-06T09:30:00.000Z",
"level": 352.84, "level": 352.84,
"flow": 2.53,
"inflow": 0,
"volume": 0,
"temperature": 17.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 352.84,
"flow": 2.53,
"inflow": 0,
"volume": 0,
"temperature": 17.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 352.84,
"flow": 0, "flow": 0,
"inflow": 1.47, "inflow": 1.47,
"volume": 32.32, "volume": 32.32,
"temperature": 19, "temperature": 19.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 352.84,
"flow": 2.53,
"inflow": 0,
"volume": 0,
"temperature": 19.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 352.84,
"flow": 2.53,
"inflow": 0,
"volume": 0,
"temperature": 19.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:20:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 1.47,
"volume": 32.3,
"temperature": 20,
"precipitation": 0 "precipitation": 0
} }
] ]
+46 -1
View File
@@ -7195,8 +7195,53 @@
"level": 369.83, "level": 369.83,
"flow": 2.5, "flow": 2.5,
"inflow": 0, "inflow": 0,
"volume": 0,
"temperature": 18,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 369.83,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 18,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 20.36, "volume": 20.36,
"temperature": 19.1, "temperature": 19.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 19.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 19.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:20:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 20.4,
"temperature": 20,
"precipitation": 0 "precipitation": 0
} }
] ]
+46 -1
View File
@@ -7194,9 +7194,54 @@
"timestamp": "2026-06-06T09:30:00.000Z", "timestamp": "2026-06-06T09:30:00.000Z",
"level": 352.53, "level": 352.53,
"flow": 19.05, "flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 17.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 352.53,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 17.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 352.52,
"flow": 19.05,
"inflow": 13.43, "inflow": 13.43,
"volume": 2.78, "volume": 2.78,
"temperature": 18.8, "temperature": 19.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 352.52,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 19.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 352.51,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 19.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:20:00.000Z",
"level": 352.51,
"flow": 19.05,
"inflow": 13.43,
"volume": 2.77,
"temperature": 19.7,
"precipitation": 0 "precipitation": 0
} }
] ]
+46 -1
View File
@@ -7175,10 +7175,55 @@
{ {
"timestamp": "2026-06-06T09:30:00.000Z", "timestamp": "2026-06-06T09:30:00.000Z",
"level": 723.09, "level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 16.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 16.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 9.25,
"volume": 199.67,
"temperature": 18.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 18.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 18.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:20:00.000Z",
"level": 723.09,
"flow": 0, "flow": 0,
"inflow": 9.25, "inflow": 9.25,
"volume": 199.67, "volume": 199.67,
"temperature": 17.7, "temperature": 18.5,
"precipitation": 0 "precipitation": 0
} }
] ]
+48 -3
View File
@@ -7175,7 +7175,7 @@
{ {
"timestamp": "2026-06-06T09:10:00.000Z", "timestamp": "2026-06-06T09:10:00.000Z",
"level": 559.03, "level": 559.03,
"flow": 0, "flow": 7.15,
"inflow": 0, "inflow": 0,
"volume": 0, "volume": 0,
"temperature": 18.1, "temperature": 18.1,
@@ -7184,7 +7184,7 @@
{ {
"timestamp": "2026-06-06T09:20:00.000Z", "timestamp": "2026-06-06T09:20:00.000Z",
"level": 559.02, "level": 559.02,
"flow": 0, "flow": 7.15,
"inflow": 0, "inflow": 0,
"volume": 0, "volume": 0,
"temperature": 18.1, "temperature": 18.1,
@@ -7194,9 +7194,54 @@
"timestamp": "2026-06-06T09:30:00.000Z", "timestamp": "2026-06-06T09:30:00.000Z",
"level": 559, "level": 559,
"flow": 0, "flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 18.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 558.99,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 18.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 558.98,
"flow": 0,
"inflow": 5.37, "inflow": 5.37,
"volume": 0.47, "volume": 0.47,
"temperature": 19.2, "temperature": 19.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 558.96,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 19.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 558.94,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 19.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:20:00.000Z",
"level": 558.93,
"flow": 0,
"inflow": 5.37,
"volume": 0.45,
"temperature": 20,
"precipitation": 0 "precipitation": 0
} }
] ]
+37 -1
View File
@@ -7194,9 +7194,45 @@
"timestamp": "2026-06-06T09:30:00.000Z", "timestamp": "2026-06-06T09:30:00.000Z",
"level": 345.27, "level": 345.27,
"flow": 0, "flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 17.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 345.27,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 17.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 345.27,
"flow": 0,
"inflow": 24.39, "inflow": 24.39,
"volume": 522.32, "volume": 522.32,
"temperature": 19.2, "temperature": 20,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 345.27,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 20,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 345.27,
"flow": 0,
"inflow": 24.39,
"volume": 522.32,
"temperature": 20.3,
"precipitation": 0 "precipitation": 0
} }
] ]
+47 -2
View File
@@ -7203,9 +7203,54 @@
"timestamp": "2026-06-06T09:30:00.000Z", "timestamp": "2026-06-06T09:30:00.000Z",
"level": 269.88, "level": 269.88,
"flow": 0, "flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 18.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 18.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 81.06, "inflow": 81.06,
"volume": 261.07, "volume": 261.23,
"temperature": 19.9, "temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:20:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 81.06,
"volume": 261.23,
"temperature": 21.1,
"precipitation": 0 "precipitation": 0
} }
] ]
+47 -2
View File
@@ -7203,9 +7203,54 @@
"timestamp": "2026-06-06T09:30:00.000Z", "timestamp": "2026-06-06T09:30:00.000Z",
"level": 217.07, "level": 217.07,
"flow": 0, "flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 18.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:40:00.000Z",
"level": 217.07,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 18.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T09:50:00.000Z",
"level": 217.07,
"flow": 0,
"inflow": 48.25, "inflow": 48.25,
"volume": 8.27, "volume": 8.23,
"temperature": 19.6, "temperature": 20.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:00:00.000Z",
"level": 217.05,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 20.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:10:00.000Z",
"level": 217.04,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 20.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T10:20:00.000Z",
"level": 217.06,
"flow": 0,
"inflow": 48.25,
"volume": 8.23,
"temperature": 20.8,
"precipitation": 0 "precipitation": 0
} }
] ]
+56 -56
View File
@@ -33,28 +33,28 @@
"name": "Lipno II", "name": "Lipno II",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "559.00", "level": "558.93",
"capacity": 31.3, "capacity": 30,
"storageDiff": -1.5, "storageDiff": -1.57,
"inflow": "5.4", "inflow": "5.4",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.47, "volume": 0.45,
"maxVolume": 1.5, "maxVolume": 1.5,
"lat": 48.625, "lat": 48.625,
"lng": 14.318, "lng": 14.318,
"sparkline": [ "sparkline": [
559.18,
559.16,
559.13,
559.12,
559.11,
559.09, 559.09,
559.08, 559.08,
559.06, 559.06,
559.05, 559.05,
559.03, 559.03,
559.02, 559.02,
559 559,
558.99,
558.98,
558.96,
558.94,
558.93
] ]
}, },
{ {
@@ -62,28 +62,28 @@
"name": "Hněvkovice", "name": "Hněvkovice",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "369.83", "level": "369.84",
"capacity": 96.5, "capacity": 96.7,
"storageDiff": -0.27, "storageDiff": -0.26,
"inflow": "0.0", "inflow": "0.0",
"outflow": "2.5", "outflow": "2.5",
"volume": 20.36, "volume": 20.4,
"maxVolume": 21.1, "maxVolume": 21.1,
"lat": 49.183, "lat": 49.183,
"lng": 14.444, "lng": 14.444,
"sparkline": [ "sparkline": [
369.83, 369.83,
369.83, 369.83,
369.81,
369.81,
369.83, 369.83,
369.83, 369.83,
369.83, 369.83,
369.83, 369.83,
369.83, 369.83,
369.83, 369.83,
369.83, 369.84,
369.83 369.84,
369.84,
369.84
] ]
}, },
{ {
@@ -91,28 +91,28 @@
"name": "Kořensko", "name": "Kořensko",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "352.53", "level": "352.51",
"capacity": 99.3, "capacity": 98.9,
"storageDiff": -0.07, "storageDiff": -0.09,
"inflow": "13.4", "inflow": "13.4",
"outflow": "19.1", "outflow": "19.1",
"volume": 2.78, "volume": 2.77,
"maxVolume": 2.8, "maxVolume": 2.8,
"lat": 49.255, "lat": 49.255,
"lng": 14.398, "lng": 14.398,
"sparkline": [ "sparkline": [
352.56,
352.56,
352.56,
352.56,
352.56,
352.56, 352.56,
352.56, 352.56,
352.55, 352.55,
352.55, 352.55,
352.54, 352.54,
352.54, 352.54,
352.53 352.53,
352.53,
352.52,
352.52,
352.51,
352.51
] ]
}, },
{ {
@@ -130,9 +130,9 @@
"lat": 49.606, "lat": 49.606,
"lng": 14.17, "lng": 14.17,
"sparkline": [ "sparkline": [
345.26, 345.27,
345.26, 345.27,
345.26, 345.27,
345.27, 345.27,
345.27, 345.27,
345.27, 345.27,
@@ -150,26 +150,26 @@
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "269.88", "level": "269.88",
"capacity": 96.9, "capacity": 97,
"storageDiff": -0.72, "storageDiff": -0.72,
"inflow": "81.1", "inflow": "81.1",
"outflow": "0.0", "outflow": "0.0",
"volume": 261.07, "volume": 261.23,
"maxVolume": 269.3, "maxVolume": 269.3,
"lat": 49.822, "lat": 49.822,
"lng": 14.436, "lng": 14.436,
"sparkline": [ "sparkline": [
269.89,
269.89,
269.89,
269.89,
269.88,
269.87, 269.87,
269.87, 269.87,
269.88, 269.88,
269.88, 269.88,
269.88, 269.88,
269.88, 269.88,
269.88,
269.89,
269.89,
269.89,
269.89,
269.88 269.88
] ]
}, },
@@ -178,28 +178,28 @@
"name": "Štěchovice", "name": "Štěchovice",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "217.07", "level": "217.06",
"capacity": 73.8, "capacity": 73.5,
"storageDiff": -2.33, "storageDiff": -2.34,
"inflow": "48.3", "inflow": "48.3",
"outflow": "0.0", "outflow": "0.0",
"volume": 8.27, "volume": 8.23,
"maxVolume": 11.2, "maxVolume": 11.2,
"lat": 49.845, "lat": 49.845,
"lng": 14.412, "lng": 14.412,
"sparkline": [ "sparkline": [
217.23,
217.22,
217.19,
217.17,
217.17,
217.14, 217.14,
217.13, 217.13,
217.12, 217.12,
217.1, 217.1,
217.09, 217.09,
217.1, 217.1,
217.07 217.07,
217.07,
217.07,
217.05,
217.04,
217.06
] ]
}, },
{ {
@@ -217,8 +217,8 @@
"lat": 48.847, "lat": 48.847,
"lng": 14.487, "lng": 14.487,
"sparkline": [ "sparkline": [
467.73, 467.74,
467.73, 467.74,
467.74, 467.74,
467.74, 467.74,
467.74, 467.74,
@@ -240,19 +240,19 @@
"capacity": 57, "capacity": 57,
"storageDiff": -16.66, "storageDiff": -16.66,
"inflow": "1.5", "inflow": "1.5",
"outflow": "0.0", "outflow": "2.5",
"volume": 32.32, "volume": 32.3,
"maxVolume": 56.7, "maxVolume": 56.7,
"lat": 49.789, "lat": 49.789,
"lng": 13.155, "lng": 13.155,
"sparkline": [ "sparkline": [
352.84,
352.84,
352.84,
352.84, 352.84,
352.85, 352.85,
352.84, 352.84,
352.85, 352.84,
352.84,
352.84,
352.84,
352.84, 352.84,
352.84, 352.84,
352.84, 352.84,
+1449
View File
File diff suppressed because one or more lines are too long
+629
View File
File diff suppressed because one or more lines are too long
+46
View File
@@ -0,0 +1,46 @@
import axios from 'axios';
import fs from 'fs';
import https from 'https';
import * as cheerio from 'cheerio';
const URL = 'https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?oid=1&id=VLL1';
async function testPostback() {
const agent = new https.Agent({ rejectUnauthorized: false });
const res = await axios.get(URL, { httpsAgent: agent, timeout: 10000 });
const $ = cheerio.load(res.data);
const viewstate = $('#__VIEWSTATE').val();
const viewstategenerator = $('#__VIEWSTATEGENERATOR').val();
const eventvalidation = $('#__EVENTVALIDATION').val();
// Try to POST for monthly data
const postData = new URLSearchParams();
postData.append('__EVENTTARGET', 'ctl00$ObsahCPH$PrechodNaBilancniData');
postData.append('__EVENTARGUMENT', '');
postData.append('__VIEWSTATE', viewstate as string);
postData.append('__VIEWSTATEGENERATOR', viewstategenerator as string);
postData.append('__EVENTVALIDATION', eventvalidation as string);
const postRes = await axios.post(URL, postData.toString(), {
httpsAgent: agent,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
fs.writeFileSync('pvl_raw_month.html', postRes.data);
console.log('Saved monthly data to pvl_raw_month.html');
const $post = cheerio.load(postRes.data);
const rows = $post('table.tabulka-seznam tr:not(:first-child)');
console.log(`Found ${rows.length} rows in the table.`);
if (rows.length > 0) {
const firstRow = rows.first().find('td').first().text().trim();
const lastRow = rows.last().find('td').first().text().trim();
console.log(`Date range: ${firstRow} to ${lastRow}`);
}
}
testPostback().catch(console.error);
+41 -8
View File
@@ -78,23 +78,26 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
}); });
const records: DataRecord[] = []; const records: DataRecord[] = [];
const parseTable = (htmlContent: string) => {
const _$ = cheerio.load(htmlContent);
let dataTable = null; let dataTable = null;
$('table').each((i, tbl) => { _$('table').each((i, tbl) => {
if ($(tbl).text().includes('Datum') && $(tbl).text().includes('Odtok')) { if (_$(tbl).text().includes('Datum') && _$(tbl).text().includes('Odtok')) {
dataTable = $(tbl); dataTable = _$(tbl);
} }
}); });
if (dataTable) { if (dataTable) {
dataTable.find('tr').each((i, row) => { dataTable.find('tr').each((i, row) => {
if (i === 0) return; // skip header if (i === 0) return; // skip header
const cols = $(row).find('td'); const cols = _$(row).find('td');
if (cols.length >= 3) { if (cols.length >= 3) {
const rawDate = $(cols[0]).text().trim(); const rawDate = _$(cols[0]).text().trim();
const levelStr = $(cols[1]).text().trim().replace(',', '.'); const levelStr = _$(cols[1]).text().trim().replace(',', '.');
let flowStr = $(cols[2]).text().trim().replace(',', '.'); let flowStr = _$(cols[2]).text().trim().replace(',', '.');
if (flowStr === '' && cols.length >= 4) { if (flowStr === '' && cols.length >= 4) {
flowStr = $(cols[3]).text().trim().replace(',', '.'); flowStr = _$(cols[3]).text().trim().replace(',', '.');
} }
const parsedDateStr = parseDateString(rawDate); const parsedDateStr = parseDateString(rawDate);
@@ -110,6 +113,36 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
} }
}); });
} }
};
// 1. Zpracování týdenních dat (GET)
parseTable(response.data);
// 2. Získání a zpracování měsíčních dat (POST)
try {
const viewstate = $('#__VIEWSTATE').val();
const viewstategenerator = $('#__VIEWSTATEGENERATOR').val();
const eventvalidation = $('#__EVENTVALIDATION').val();
if (viewstate && viewstategenerator && eventvalidation) {
const postData = new URLSearchParams();
postData.append('__EVENTTARGET', 'ctl00$ObsahCPH$PrechodNaBilancniData');
postData.append('__EVENTARGUMENT', '');
postData.append('__VIEWSTATE', viewstate as string);
postData.append('__VIEWSTATEGENERATOR', viewstategenerator as string);
postData.append('__EVENTVALIDATION', eventvalidation as string);
const postRes = await axios.post(URL, postData.toString(), {
httpsAgent: agent,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 10000
});
parseTable(postRes.data);
}
} catch (err: any) {
console.warn(`Failed to fetch monthly data for ${internalId}:`, err.message);
}
if (records.length > 0) { if (records.length > 0) {
records[0].inflow = currentInflow; records[0].inflow = currentInflow;
+16 -2
View File
@@ -7,7 +7,7 @@ import FavoritesOverview from './components/FavoritesOverview';
import Sidebar from './components/Sidebar'; import Sidebar from './components/Sidebar';
import Topbar from './components/Topbar'; import Topbar from './components/Topbar';
import SettingsModal from './components/SettingsModal'; import SettingsModal from './components/SettingsModal';
import { type Language } from './translations'; import { type Language, t } from './translations';
import { lakesConfig } from '../scripts/lakesConfig'; import { lakesConfig } from '../scripts/lakesConfig';
import { slugify } from './utils/slugify'; import { slugify } from './utils/slugify';
import './App.css'; import './App.css';
@@ -66,8 +66,9 @@ function App() {
onCloseMobileMenu={() => setIsMobileMenuOpen(false)} onCloseMobileMenu={() => setIsMobileMenuOpen(false)}
/> />
<div className="main-content"> <div className="main-content" style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<Topbar language={language} onToggleMobileMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)} /> <Topbar language={language} onToggleMobileMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)} />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Routes> <Routes>
<Route path="/" element={<LakesOverview language={language} />} /> <Route path="/" element={<LakesOverview language={language} />} />
<Route path="/favorites" element={<FavoritesOverview language={language} />} /> <Route path="/favorites" element={<FavoritesOverview language={language} />} />
@@ -75,6 +76,19 @@ function App() {
<Route path="/:slug" element={<LakeDetailWrapper language={language} />} /> <Route path="/:slug" element={<LakeDetailWrapper language={language} />} />
</Routes> </Routes>
</div> </div>
<footer style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1.5rem',
fontSize: '0.8rem',
color: 'var(--text-muted)',
marginTop: 'auto'
}}>
<span>Zdroje dat: pvl.cz, open-meteo.com</span>
<span>{t[language].chart.createdIn}</span>
</footer>
</div>
{isSettingsOpen && ( {isSettingsOpen && (
<SettingsModal <SettingsModal
+8 -7
View File
@@ -84,7 +84,8 @@ const LakeDetail = ({ language, lakeId }: Props) => {
const topbarDict = t[language].topbar; const topbarDict = t[language].topbar;
useEffect(() => { useEffect(() => {
fetch('/data/lakes_index.json') const loadData = () => {
fetch(`/data/lakes_index.json?t=${Date.now()}`)
.then(res => res.json()) .then(res => res.json())
.then(indexData => { .then(indexData => {
const found = indexData.find((l: any) => l.id === lakeId); const found = indexData.find((l: any) => l.id === lakeId);
@@ -94,7 +95,7 @@ const LakeDetail = ({ language, lakeId }: Props) => {
const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1'; const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1';
fetch(`/data/${internalId}.json`) fetch(`/data/${internalId}.json?t=${Date.now()}`)
.then(res => res.json()) .then(res => res.json())
.then(json => { .then(json => {
const formattedData = json.map((item: any) => { const formattedData = json.map((item: any) => {
@@ -122,6 +123,11 @@ const LakeDetail = ({ language, lakeId }: Props) => {
console.error('Failed to load data', err); console.error('Failed to load data', err);
setLoading(false); setLoading(false);
}); });
};
loadData();
const intervalId = setInterval(loadData, 60 * 1000); // Poll every 1 minute
return () => clearInterval(intervalId);
}, [language, lakeId]); }, [language, lakeId]);
if (loading) { if (loading) {
@@ -336,11 +342,6 @@ const LakeDetail = ({ language, lakeId }: Props) => {
</div> </div>
</div> </div>
</div> </div>
<div className="dashboard-footer" style={{ marginTop: '0' }}>
<span>{dict.dataSources} <a href="https://www.chmi.cz/" target="_blank" rel="noreferrer">ČHMÚ</a>, <a href="https://www.pvl.cz/" target="_blank" rel="noreferrer">pvl.cz</a></span>
<span>{dict.createdIn}</span>
</div>
</div> </div>
); );
}; };
+6
View File
@@ -170,10 +170,16 @@ const LakesOverview = ({ language }: Props) => {
const { isFavorite, toggleFavorite, favorites } = useFavorites(); const { isFavorite, toggleFavorite, favorites } = useFavorites();
useEffect(() => { useEffect(() => {
const loadData = () => {
fetch(`/data/lakes_index.json?t=${Date.now()}`) fetch(`/data/lakes_index.json?t=${Date.now()}`)
.then(res => res.json()) .then(res => res.json())
.then(data => setLakes(data)) .then(data => setLakes(data))
.catch(err => console.error(err)); .catch(err => console.error(err));
};
loadData();
const intervalId = setInterval(loadData, 60 * 1000); // Poll every 1 minute
return () => clearInterval(intervalId);
}, []); }, []);
const favoriteLakes = lakes.filter(l => isFavorite(l.id)); const favoriteLakes = lakes.filter(l => isFavorite(l.id));
+5
View File
@@ -1,5 +1,6 @@
import { FiSearch, FiMenu, FiDroplet } from 'react-icons/fi'; import { FiSearch, FiMenu, FiDroplet } from 'react-icons/fi';
import { type Language, t } from '../translations'; import { type Language, t } from '../translations';
import { useLocation } from 'react-router-dom';
interface Props { interface Props {
language: Language; language: Language;
@@ -8,6 +9,8 @@ interface Props {
const Topbar = ({ language, onToggleMobileMenu }: Props) => { const Topbar = ({ language, onToggleMobileMenu }: Props) => {
const dict = t[language].topbar; const dict = t[language].topbar;
const location = useLocation();
const showSearch = location.pathname === '/' || location.pathname === '/favorites';
return ( return (
<div className="topbar"> <div className="topbar">
@@ -19,10 +22,12 @@ const Topbar = ({ language, onToggleMobileMenu }: Props) => {
<span>Hladinator</span> <span>Hladinator</span>
</div> </div>
{showSearch && (
<div className="search-bar"> <div className="search-bar">
<FiSearch /> <FiSearch />
<input type="text" placeholder={dict.search} /> <input type="text" placeholder={dict.search} />
</div> </div>
)}
</div> </div>
</div> </div>
); );