From 07a7ec678798dc5495be5f7f9a4ab3d9ebc248e4 Mon Sep 17 00:00:00 2001 From: Braulio Date: Tue, 24 Feb 2026 09:47:20 +0100 Subject: [PATCH 1/7] integracion-mino --- .../scraping-cuenca-mino-sil/package.json | 2 + .../src/console-runner.ts | 3 +- .../src/integration.ts | 86 ++++++++++++++----- package-lock.json | 12 +-- 4 files changed, 73 insertions(+), 30 deletions(-) diff --git a/integrations/scraping-cuenca-mino-sil/package.json b/integrations/scraping-cuenca-mino-sil/package.json index c728ec0..4cf93ba 100644 --- a/integrations/scraping-cuenca-mino-sil/package.json +++ b/integrations/scraping-cuenca-mino-sil/package.json @@ -10,6 +10,8 @@ "start": "tsx --watch src/console-runner.ts" }, "dependencies": { + "axios": "^1.7.0", + "cheerio": "^1.1.2", "db-model": "^1.0.0" } } diff --git a/integrations/scraping-cuenca-mino-sil/src/console-runner.ts b/integrations/scraping-cuenca-mino-sil/src/console-runner.ts index d88d339..617d734 100644 --- a/integrations/scraping-cuenca-mino-sil/src/console-runner.ts +++ b/integrations/scraping-cuenca-mino-sil/src/console-runner.ts @@ -1,5 +1,4 @@ import { getEstadoCuencaMinoSil } from "./integration"; console.log("Estado de la Cuenca Miño Sil:"); -const result = await getEstadoCuencaMinoSil(); -console.log(JSON.stringify(result, null, 2)); +await getEstadoCuencaMinoSil(); diff --git a/integrations/scraping-cuenca-mino-sil/src/integration.ts b/integrations/scraping-cuenca-mino-sil/src/integration.ts index 783ff90..b3126ad 100644 --- a/integrations/scraping-cuenca-mino-sil/src/integration.ts +++ b/integrations/scraping-cuenca-mino-sil/src/integration.ts @@ -1,24 +1,66 @@ -import type { Embalse } from "db-model"; - -export const getEstadoCuencaMinoSil = async (): Promise => { - return [ - { - id: "1", - nombre: "Embalse de Belesar", - provincia: "Lugo", - capacidad: 3000000000, - nivelActual: 2500000000, - fechaUltimoNivel: new Date("2023-10-01"), - porcentajeLlenado: 83.3, - }, - { - id: "2", - nombre: "Embalse de Velle", - provincia: "Ourense", - capacidad: 500000000, - nivelActual: 400000000, - fechaUltimoNivel: new Date("2023-10-01"), - porcentajeLlenado: 80.0, +import axios from "axios"; +import https from "https"; +import * as cheerio from "cheerio"; + +const httpsAgent = new https.Agent({ + rejectUnauthorized: false, +}); + +const browserHeaders = { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + Accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "es-ES,es;q=0.9,en;q=0.8", +}; + +const BASE_URL = + "https://saih.chminosil.es/index.php?url=/datos/mapas/mapa:H1/area:HID/acc:1"; +const TARGET_URL = + "https://saih.chminosil.es/index.php?url=/datos/situacionEmbalses"; + +export const getEstadoCuencaMinoSil = async (): Promise => { + // Step 1: Visit base page to get session cookies (don't follow redirects) + const sessionResponse = await axios.get(BASE_URL, { + httpsAgent, + maxRedirects: 0, + headers: browserHeaders, + validateStatus: (status) => status >= 200 && status < 400, + }); + + const setCookieHeaders = sessionResponse.headers["set-cookie"]; + const cookieString = setCookieHeaders + ? setCookieHeaders + .map((cookie: string) => cookie.split(";")[0]) + .join("; ") + : ""; + + // Step 2: Fetch the target page with session cookies (follow redirects) + const { data: html } = await axios.get(TARGET_URL, { + httpsAgent, + maxRedirects: 10, + headers: { + ...browserHeaders, + Cookie: cookieString, + Referer: BASE_URL, }, - ]; + }); + + // Step 3: Parse HTML with Cheerio + const $ = cheerio.load(html); + + // Step 4: Iterate table rows, skip headers + $("table.tabla tr").each((_index, row) => { + const cells = $(row).find("td"); + if (cells.length === 0) return; // Skip header rows with + + const nombre = $(cells[1]).text().trim(); + if (!nombre || !nombre.includes(" - ")) return; // Skip totals row + const capacidadTotal = $(cells[4]).text().trim(); + const volumenActual = $(cells[6]).text().trim(); + + console.log( + `Embalse: ${nombre} | Capacidad Total: ${capacidadTotal} hm³ | Volumen Actual: ${volumenActual} hm³` + ); + }); }; diff --git a/package-lock.json b/package-lock.json index bdf99ea..99ae91b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -175,6 +175,8 @@ "integrations/scraping-cuenca-mino-sil": { "version": "1.0.0", "dependencies": { + "axios": "^1.7.0", + "cheerio": "^1.1.2", "db-model": "^1.0.0" } }, @@ -5853,17 +5855,15 @@ "arcgis": "*", "db-model": "*", "mongodb": "^6.19.0", - "scraping-cuenca-mediterranea": "*" - }, - "devDependencies": { - "@types/prompts": "^2.4.9", - "prompts": "^2.4.2", "scraping-cuenca-cantabrico": "*", "scraping-cuenca-catalana": "*", "scraping-cuenca-duero": "*", - "scraping-cuenca-guadalquivir": "*", "scraping-cuenca-jucar": "*", "scraping-cuenca-mediterranea": "*" + }, + "devDependencies": { + "@types/prompts": "^2.4.9", + "prompts": "^2.4.2" } }, "packages/db-model": { From bc48498ca54567ec97ac5e2815e76f03bb2f16f0 Mon Sep 17 00:00:00 2001 From: Antonio Contreras Date: Wed, 25 Feb 2026 11:35:16 +0100 Subject: [PATCH 2/7] add cuenca model --- .../scraping-cuenca-mino-sil/src/api/cuenca.model.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 integrations/scraping-cuenca-mino-sil/src/api/cuenca.model.ts diff --git a/integrations/scraping-cuenca-mino-sil/src/api/cuenca.model.ts b/integrations/scraping-cuenca-mino-sil/src/api/cuenca.model.ts new file mode 100644 index 0000000..1489d40 --- /dev/null +++ b/integrations/scraping-cuenca-mino-sil/src/api/cuenca.model.ts @@ -0,0 +1,7 @@ +export interface EmbalsesMinoSil { + id: number; + embalse: string; + capacidadTotalHm3: number; + volumenActualHm3: number; + fecha: string; +} From 998cb4cf02092a7c78a40a13b2513ab37a056a0d Mon Sep 17 00:00:00 2001 From: Antonio Contreras Date: Wed, 25 Feb 2026 18:13:16 +0100 Subject: [PATCH 3/7] =?UTF-8?q?implement=20scraping=20functionality=20for?= =?UTF-8?q?=20Cuenca=20Mi=C3=B1o=20Sil=20and=20update=20package-lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/cuenca.api.ts | 47 +++++++ .../scraping-cuenca-mino-sil/src/api/index.ts | 2 + .../src/console-runner.ts | 4 +- .../src/integration.ts | 119 ++++++++++-------- .../src/scraper/business.ts | 31 +++++ .../src/scraper/index.ts | 1 + package-lock.json | 8 ++ 7 files changed, 155 insertions(+), 57 deletions(-) create mode 100644 integrations/scraping-cuenca-mino-sil/src/api/cuenca.api.ts create mode 100644 integrations/scraping-cuenca-mino-sil/src/api/index.ts create mode 100644 integrations/scraping-cuenca-mino-sil/src/scraper/business.ts create mode 100644 integrations/scraping-cuenca-mino-sil/src/scraper/index.ts diff --git a/integrations/scraping-cuenca-mino-sil/src/api/cuenca.api.ts b/integrations/scraping-cuenca-mino-sil/src/api/cuenca.api.ts new file mode 100644 index 0000000..2f26280 --- /dev/null +++ b/integrations/scraping-cuenca-mino-sil/src/api/cuenca.api.ts @@ -0,0 +1,47 @@ +import axios from "axios"; +import https from "https"; + +const httpsAgent = new https.Agent({ + rejectUnauthorized: false, +}); + +const browserHeaders = { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + Accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "es-ES,es;q=0.9,en;q=0.8", +}; + +const BASE_URL = + "https://saih.chminosil.es/index.php?url=/datos/mapas/mapa:H1/area:HID/acc:1"; +const TARGET_URL = + "https://saih.chminosil.es/index.php?url=/datos/situacionEmbalses"; + +export async function getCuencaPageHTMLContent(): Promise { + const sessionResponse = await axios.get(BASE_URL, { + httpsAgent, + maxRedirects: 0, + headers: browserHeaders, + validateStatus: (status) => status >= 200 && status < 400, + }); + + const setCookieHeaders = sessionResponse.headers["set-cookie"]; + const cookieString = setCookieHeaders + ? setCookieHeaders + .map((cookie: string) => cookie.split(";")[0]) + .join("; ") + : ""; + + const { data: html } = await axios.get(TARGET_URL, { + httpsAgent, + maxRedirects: 10, + headers: { + ...browserHeaders, + Cookie: cookieString, + Referer: BASE_URL, + }, + }); + + return html; +} diff --git a/integrations/scraping-cuenca-mino-sil/src/api/index.ts b/integrations/scraping-cuenca-mino-sil/src/api/index.ts new file mode 100644 index 0000000..b9296d5 --- /dev/null +++ b/integrations/scraping-cuenca-mino-sil/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./cuenca.api.js"; +export * from "./cuenca.model.js"; diff --git a/integrations/scraping-cuenca-mino-sil/src/console-runner.ts b/integrations/scraping-cuenca-mino-sil/src/console-runner.ts index 617d734..ea3e043 100644 --- a/integrations/scraping-cuenca-mino-sil/src/console-runner.ts +++ b/integrations/scraping-cuenca-mino-sil/src/console-runner.ts @@ -1,4 +1,4 @@ -import { getEstadoCuencaMinoSil } from "./integration"; +import { scrapeCuencaMediterranea } from "./integration"; console.log("Estado de la Cuenca Miño Sil:"); -await getEstadoCuencaMinoSil(); +await scrapeCuencaMediterranea(); diff --git a/integrations/scraping-cuenca-mino-sil/src/integration.ts b/integrations/scraping-cuenca-mino-sil/src/integration.ts index b3126ad..9b33024 100644 --- a/integrations/scraping-cuenca-mino-sil/src/integration.ts +++ b/integrations/scraping-cuenca-mino-sil/src/integration.ts @@ -1,66 +1,75 @@ -import axios from "axios"; -import https from "https"; import * as cheerio from "cheerio"; +import { EmbalseUpdateSAIHEntity } from "db-model"; +import { getCuencaPageHTMLContent } from "./api/index.js"; +import { extractProvinceTables } from "./scraper/index.js"; -const httpsAgent = new https.Agent({ - rejectUnauthorized: false, -}); +export async function scrapeCuencaMediterranea(): Promise { + const html = await getCuencaPageHTMLContent(); + const $: cheerio.CheerioAPI = cheerio.load(html); + const rawEmbalses = extractProvinceTables($); -const browserHeaders = { - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", - Accept: - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Language": "es-ES,es;q=0.9,en;q=0.8", -}; + console.log("Embalses extraídos:", rawEmbalses); +} -const BASE_URL = - "https://saih.chminosil.es/index.php?url=/datos/mapas/mapa:H1/area:HID/acc:1"; -const TARGET_URL = - "https://saih.chminosil.es/index.php?url=/datos/situacionEmbalses"; +// const httpsAgent = new https.Agent({ +// rejectUnauthorized: false, +// }); -export const getEstadoCuencaMinoSil = async (): Promise => { - // Step 1: Visit base page to get session cookies (don't follow redirects) - const sessionResponse = await axios.get(BASE_URL, { - httpsAgent, - maxRedirects: 0, - headers: browserHeaders, - validateStatus: (status) => status >= 200 && status < 400, - }); +// const browserHeaders = { +// "User-Agent": +// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", +// Accept: +// "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", +// "Accept-Language": "es-ES,es;q=0.9,en;q=0.8", +// }; - const setCookieHeaders = sessionResponse.headers["set-cookie"]; - const cookieString = setCookieHeaders - ? setCookieHeaders - .map((cookie: string) => cookie.split(";")[0]) - .join("; ") - : ""; +// const BASE_URL = +// "https://saih.chminosil.es/index.php?url=/datos/mapas/mapa:H1/area:HID/acc:1"; +// const TARGET_URL = +// "https://saih.chminosil.es/index.php?url=/datos/situacionEmbalses"; - // Step 2: Fetch the target page with session cookies (follow redirects) - const { data: html } = await axios.get(TARGET_URL, { - httpsAgent, - maxRedirects: 10, - headers: { - ...browserHeaders, - Cookie: cookieString, - Referer: BASE_URL, - }, - }); +// export const getEstadoCuencaMinoSil = async (): Promise => { +// // Step 1: Visit base page to get session cookies (don't follow redirects) +// const sessionResponse = await axios.get(BASE_URL, { +// httpsAgent, +// maxRedirects: 0, +// headers: browserHeaders, +// validateStatus: (status) => status >= 200 && status < 400, +// }); - // Step 3: Parse HTML with Cheerio - const $ = cheerio.load(html); +// const setCookieHeaders = sessionResponse.headers["set-cookie"]; +// const cookieString = setCookieHeaders +// ? setCookieHeaders +// .map((cookie: string) => cookie.split(";")[0]) +// .join("; ") +// : ""; - // Step 4: Iterate table rows, skip headers - $("table.tabla tr").each((_index, row) => { - const cells = $(row).find("td"); - if (cells.length === 0) return; // Skip header rows with +// // Step 2: Fetch the target page with session cookies (follow redirects) +// const { data: html } = await axios.get(TARGET_URL, { +// httpsAgent, +// maxRedirects: 10, +// headers: { +// ...browserHeaders, +// Cookie: cookieString, +// Referer: BASE_URL, +// }, +// }); - const nombre = $(cells[1]).text().trim(); - if (!nombre || !nombre.includes(" - ")) return; // Skip totals row - const capacidadTotal = $(cells[4]).text().trim(); - const volumenActual = $(cells[6]).text().trim(); +// // Step 3: Parse HTML with Cheerio +// const $ = cheerio.load(html); - console.log( - `Embalse: ${nombre} | Capacidad Total: ${capacidadTotal} hm³ | Volumen Actual: ${volumenActual} hm³` - ); - }); -}; +// // Step 4: Iterate table rows, skip headers +// $("table.tabla tr").each((_index, row) => { +// const cells = $(row).find("td"); +// if (cells.length === 0) return; // Skip header rows with + +// const nombre = $(cells[1]).text().trim(); +// if (!nombre || !nombre.includes(" - ")) return; // Skip totals row +// const capacidadTotal = $(cells[4]).text().trim(); +// const volumenActual = $(cells[6]).text().trim(); + +// console.log( +// `Embalse: ${nombre} | Capacidad Total: ${capacidadTotal} hm³ | Volumen Actual: ${volumenActual} hm³` +// ); +// }); +// }; diff --git a/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts b/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts new file mode 100644 index 0000000..ad9782c --- /dev/null +++ b/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts @@ -0,0 +1,31 @@ +import * as cheerio from "cheerio"; +import { EmbalsesMinoSil } from '../api/index.js'; + +export function extractProvinceTables( + $: cheerio.CheerioAPI +): EmbalsesMinoSil[] { + + const embalses: EmbalsesMinoSil[] = []; + + $("table.tabla tr").each((_index, row) => { + const cells = $(row).find("td"); + if (cells.length === 0) return; // Skip header rows with + + const nombre = $(cells[1]).text().trim(); + if (!nombre || !nombre.includes(" - ")) return; // Skip totals row + const id = nombre.split(" - ")[0].trim(); + const capacidadTotal = $(cells[4]).text().trim(); + const fecha = $(cells[0]).text().trim(); + const volumenActual = $(cells[6]).text().trim(); + + embalses.push({ + id: Number(id), + embalse: nombre, + capacidadTotalHm3: Number(capacidadTotal), + volumenActualHm3: Number(volumenActual), + fecha: fecha + }); + }); + + return embalses; +} diff --git a/integrations/scraping-cuenca-mino-sil/src/scraper/index.ts b/integrations/scraping-cuenca-mino-sil/src/scraper/index.ts new file mode 100644 index 0000000..c78520f --- /dev/null +++ b/integrations/scraping-cuenca-mino-sil/src/scraper/index.ts @@ -0,0 +1 @@ +export * from './business.js'; diff --git a/package-lock.json b/package-lock.json index 99ae91b..3e09ca4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2142,6 +2142,7 @@ "version": "24.3.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.10.0" } @@ -2166,6 +2167,7 @@ "version": "19.1.10", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2965,6 +2967,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -4537,6 +4540,7 @@ "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -4695,6 +4699,7 @@ "node_modules/react": { "version": "19.1.1", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4702,6 +4707,7 @@ "node_modules/react-dom": { "version": "19.1.1", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5474,6 +5480,7 @@ "version": "5.9.2", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5505,6 +5512,7 @@ "version": "7.1.3", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", From 3034d180d020d0bc9d0bd8833251ee77268c7950 Mon Sep 17 00:00:00 2001 From: Antonio Contreras Date: Wed, 25 Feb 2026 18:46:16 +0100 Subject: [PATCH 4/7] add scraping-cuenca-mino-sil integration and update package scripts --- .../scraping-cuenca-mino-sil/package.json | 6 +- package-lock.json | 105 ++++++++++++++++++ packages/db/package.json | 3 +- 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/integrations/scraping-cuenca-mino-sil/package.json b/integrations/scraping-cuenca-mino-sil/package.json index 4cf93ba..39dd0a8 100644 --- a/integrations/scraping-cuenca-mino-sil/package.json +++ b/integrations/scraping-cuenca-mino-sil/package.json @@ -7,7 +7,11 @@ ".": "./src/index.ts" }, "scripts": { - "start": "tsx --watch src/console-runner.ts" + "start": "tsx --watch src/console-runner.ts", + "build": "run-p clean type-check build:scraping-cuenca-mino-sil", + "build:scraping-cuenca-mino-sil": "tsc", + "clean": "rimraf dist", + "type-check": "tsc --noEmit --preserveWatchOutput" }, "dependencies": { "axios": "^1.7.0", diff --git a/package-lock.json b/package-lock.json index b0eb56d..b34d59a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4926,6 +4926,111 @@ }, "packages/db-model": { "version": "1.0.0" + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/packages/db/package.json b/packages/db/package.json index c103c88..011af4b 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -30,7 +30,8 @@ "scraping-cuenca-duero": "*", "scraping-cuenca-jucar": "*", "scraping-cuenca-mediterranea": "*", - "scraping-cuenca-segura": "*" + "scraping-cuenca-segura": "*", + "scraping-cuenca-mino-sil": "*" }, "devDependencies": { "@types/prompts": "^2.4.9", From e01ef70a615a304ed46ca22cb26b91c4c5f1f5cd Mon Sep 17 00:00:00 2001 From: Antonio Contreras Date: Thu, 26 Feb 2026 12:39:57 +0100 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20update=20scraping=20functionali?= =?UTF-8?q?ty=20for=20Cuenca=20Mi=C3=B1o=20Sil=20and=20enhance=20type=20de?= =?UTF-8?q?finitions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/next-env.d.ts | 2 +- .../scraping-cuenca-mino-sil/package.json | 4 +- .../src/console-runner.ts | 5 +- .../scraping-cuenca-mino-sil/src/index.ts | 2 +- .../src/integration.ts | 67 +------------------ .../src/scraper/business.ts | 23 +++++-- .../src/scraper/helpers.ts | 5 ++ .../src/scraper/index.ts | 2 + .../src/scraper/mapper.ts | 13 ++++ .../scraping-cuenca-mino-sil/tsconfig.json | 13 ++-- 10 files changed, 57 insertions(+), 79 deletions(-) create mode 100644 integrations/scraping-cuenca-mino-sil/src/scraper/helpers.ts create mode 100644 integrations/scraping-cuenca-mino-sil/src/scraper/mapper.ts diff --git a/front/next-env.d.ts b/front/next-env.d.ts index c4b7818..9edff1c 100644 --- a/front/next-env.d.ts +++ b/front/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/integrations/scraping-cuenca-mino-sil/package.json b/integrations/scraping-cuenca-mino-sil/package.json index 39dd0a8..fbfb846 100644 --- a/integrations/scraping-cuenca-mino-sil/package.json +++ b/integrations/scraping-cuenca-mino-sil/package.json @@ -4,8 +4,10 @@ "private": true, "type": "module", "exports": { - ".": "./src/index.ts" + ".": "./dist/index.js" }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "scripts": { "start": "tsx --watch src/console-runner.ts", "build": "run-p clean type-check build:scraping-cuenca-mino-sil", diff --git a/integrations/scraping-cuenca-mino-sil/src/console-runner.ts b/integrations/scraping-cuenca-mino-sil/src/console-runner.ts index ea3e043..f4284ce 100644 --- a/integrations/scraping-cuenca-mino-sil/src/console-runner.ts +++ b/integrations/scraping-cuenca-mino-sil/src/console-runner.ts @@ -1,4 +1,5 @@ -import { scrapeCuencaMediterranea } from "./integration"; +import { scrapeCuencaMinioSil } from "./integration.js"; console.log("Estado de la Cuenca Miño Sil:"); -await scrapeCuencaMediterranea(); +const result = await scrapeCuencaMinioSil(); +console.log(result); diff --git a/integrations/scraping-cuenca-mino-sil/src/index.ts b/integrations/scraping-cuenca-mino-sil/src/index.ts index 992c96b..25e269a 100644 --- a/integrations/scraping-cuenca-mino-sil/src/index.ts +++ b/integrations/scraping-cuenca-mino-sil/src/index.ts @@ -1,2 +1,2 @@ -export * from "./integration"; +export * from "./integration.js"; diff --git a/integrations/scraping-cuenca-mino-sil/src/integration.ts b/integrations/scraping-cuenca-mino-sil/src/integration.ts index 9b33024..a5328ef 100644 --- a/integrations/scraping-cuenca-mino-sil/src/integration.ts +++ b/integrations/scraping-cuenca-mino-sil/src/integration.ts @@ -2,74 +2,13 @@ import * as cheerio from "cheerio"; import { EmbalseUpdateSAIHEntity } from "db-model"; import { getCuencaPageHTMLContent } from "./api/index.js"; import { extractProvinceTables } from "./scraper/index.js"; +import { mapToEmbalseUpdateSAIH } from './scraper/index.js'; -export async function scrapeCuencaMediterranea(): Promise { +export async function scrapeCuencaMinioSil(): Promise { const html = await getCuencaPageHTMLContent(); const $: cheerio.CheerioAPI = cheerio.load(html); const rawEmbalses = extractProvinceTables($); console.log("Embalses extraídos:", rawEmbalses); + return mapToEmbalseUpdateSAIH(rawEmbalses); } - -// const httpsAgent = new https.Agent({ -// rejectUnauthorized: false, -// }); - -// const browserHeaders = { -// "User-Agent": -// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", -// Accept: -// "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", -// "Accept-Language": "es-ES,es;q=0.9,en;q=0.8", -// }; - -// const BASE_URL = -// "https://saih.chminosil.es/index.php?url=/datos/mapas/mapa:H1/area:HID/acc:1"; -// const TARGET_URL = -// "https://saih.chminosil.es/index.php?url=/datos/situacionEmbalses"; - -// export const getEstadoCuencaMinoSil = async (): Promise => { -// // Step 1: Visit base page to get session cookies (don't follow redirects) -// const sessionResponse = await axios.get(BASE_URL, { -// httpsAgent, -// maxRedirects: 0, -// headers: browserHeaders, -// validateStatus: (status) => status >= 200 && status < 400, -// }); - -// const setCookieHeaders = sessionResponse.headers["set-cookie"]; -// const cookieString = setCookieHeaders -// ? setCookieHeaders -// .map((cookie: string) => cookie.split(";")[0]) -// .join("; ") -// : ""; - -// // Step 2: Fetch the target page with session cookies (follow redirects) -// const { data: html } = await axios.get(TARGET_URL, { -// httpsAgent, -// maxRedirects: 10, -// headers: { -// ...browserHeaders, -// Cookie: cookieString, -// Referer: BASE_URL, -// }, -// }); - -// // Step 3: Parse HTML with Cheerio -// const $ = cheerio.load(html); - -// // Step 4: Iterate table rows, skip headers -// $("table.tabla tr").each((_index, row) => { -// const cells = $(row).find("td"); -// if (cells.length === 0) return; // Skip header rows with - -// const nombre = $(cells[1]).text().trim(); -// if (!nombre || !nombre.includes(" - ")) return; // Skip totals row -// const capacidadTotal = $(cells[4]).text().trim(); -// const volumenActual = $(cells[6]).text().trim(); - -// console.log( -// `Embalse: ${nombre} | Capacidad Total: ${capacidadTotal} hm³ | Volumen Actual: ${volumenActual} hm³` -// ); -// }); -// }; diff --git a/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts b/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts index ad9782c..89e34bf 100644 --- a/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts +++ b/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts @@ -1,5 +1,16 @@ import * as cheerio from "cheerio"; import { EmbalsesMinoSil } from '../api/index.js'; +import { mapStringToApiDate } from './helpers.js'; + +export function parseEuropeanNumber(value: string): number { + if (!value || value.trim() === "" || value === "*" || value === "n/d") { + return NaN; + } + + // Replace comma with dot for decimal separator + const normalizedValue = value.replace(",", "."); + return parseFloat(normalizedValue); +} export function extractProvinceTables( $: cheerio.CheerioAPI @@ -13,17 +24,17 @@ export function extractProvinceTables( const nombre = $(cells[1]).text().trim(); if (!nombre || !nombre.includes(" - ")) return; // Skip totals row - const id = nombre.split(" - ")[0].trim(); + const id = nombre.split(" - ")[0].trim().match(/^([A-Z])(\d+)$/)[2]; // Extract ID from name const capacidadTotal = $(cells[4]).text().trim(); - const fecha = $(cells[0]).text().trim(); + const fecha = $(cells[8]).text().trim(); const volumenActual = $(cells[6]).text().trim(); embalses.push({ id: Number(id), - embalse: nombre, - capacidadTotalHm3: Number(capacidadTotal), - volumenActualHm3: Number(volumenActual), - fecha: fecha + embalse: nombre.split(" - ")[1].trim(), + capacidadTotalHm3: parseEuropeanNumber(capacidadTotal), + volumenActualHm3: parseEuropeanNumber(volumenActual), + fecha: mapStringToApiDate(fecha) }); }); diff --git a/integrations/scraping-cuenca-mino-sil/src/scraper/helpers.ts b/integrations/scraping-cuenca-mino-sil/src/scraper/helpers.ts new file mode 100644 index 0000000..eb0dca2 --- /dev/null +++ b/integrations/scraping-cuenca-mino-sil/src/scraper/helpers.ts @@ -0,0 +1,5 @@ +export const mapStringToApiDate = (strDate: string) => { + const [year, month, day] = strDate.split(" ")[0].split('/'); + + return `${day}/${month}/${year}`; +} diff --git a/integrations/scraping-cuenca-mino-sil/src/scraper/index.ts b/integrations/scraping-cuenca-mino-sil/src/scraper/index.ts index c78520f..a6e3484 100644 --- a/integrations/scraping-cuenca-mino-sil/src/scraper/index.ts +++ b/integrations/scraping-cuenca-mino-sil/src/scraper/index.ts @@ -1 +1,3 @@ export * from './business.js'; +export * from './helpers.js'; +export * from './mapper.js'; diff --git a/integrations/scraping-cuenca-mino-sil/src/scraper/mapper.ts b/integrations/scraping-cuenca-mino-sil/src/scraper/mapper.ts new file mode 100644 index 0000000..52ce291 --- /dev/null +++ b/integrations/scraping-cuenca-mino-sil/src/scraper/mapper.ts @@ -0,0 +1,13 @@ +import { EmbalseUpdateSAIHEntity } from 'db-model'; +import { EmbalsesMinoSil } from '../api/index.js'; + +export function mapToEmbalseUpdateSAIH( + embalsesMinoSil: EmbalsesMinoSil[] +): EmbalseUpdateSAIHEntity[] { + return embalsesMinoSil.map((embalse) => ({ + id: embalse.id, + nombre: embalse.embalse, + aguaActualSAIH: embalse.volumenActualHm3, + fechaMedidaSAIH: embalse.fecha, + })); +} diff --git a/integrations/scraping-cuenca-mino-sil/tsconfig.json b/integrations/scraping-cuenca-mino-sil/tsconfig.json index 5b80506..ce7b1d4 100644 --- a/integrations/scraping-cuenca-mino-sil/tsconfig.json +++ b/integrations/scraping-cuenca-mino-sil/tsconfig.json @@ -1,11 +1,16 @@ { "compilerOptions": { "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "dist", "skipLibCheck": true, "isolatedModules": true, - "esModuleInterop": true + "esModuleInterop": true, + "verbatimModuleSyntax": false, + "declaration": true, + "baseUrl": "./" }, - "include": ["src"] + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] } From cf81515c1b7687928ca3c92f628a674cad2704d3 Mon Sep 17 00:00:00 2001 From: Antonio Contreras Date: Thu, 26 Feb 2026 14:04:29 +0100 Subject: [PATCH 6/7] feat: implement scraping and updating functionality for Cuenca Mino Sil --- functions/src/functions/scraping-functions.ts | 10 ++++ .../src/integration.ts | 1 - .../src/scraper/business.ts | 8 +-- .../src/scraper/helpers.ts | 2 +- .../db/src/dals/embalses/embalses.mappers.ts | 36 +++++++++++++ .../src/dals/embalses/embalses.repository.ts | 52 ++++++++++++++++++- 6 files changed, 103 insertions(+), 6 deletions(-) diff --git a/functions/src/functions/scraping-functions.ts b/functions/src/functions/scraping-functions.ts index ef27e27..361993f 100644 --- a/functions/src/functions/scraping-functions.ts +++ b/functions/src/functions/scraping-functions.ts @@ -36,6 +36,8 @@ export async function scrapingsFunction( const responseCuencaSegura = await embalsesRepository.actualizarCuencaSegura(); + const responseCuencaMinoSil = await embalsesRepository.actualizarCuencaMinoSil(); + if (responseCuencaMediterranea) { context.log( "scrapings-function: Se han actualizado los embalses de la cuenca Mediterránea", @@ -92,6 +94,14 @@ export async function scrapingsFunction( ); } + if (responseCuencaMinoSil) { + context.log(`Se han actualizado los embalses de la cuenca Mino Sil`); + } else { + context.log( + "No se han podido actualizar los embalses de la cuenca Mino Sil" + ); + } + } catch (error) { context.error("scrapings-function: ERROR", error); throw error; diff --git a/integrations/scraping-cuenca-mino-sil/src/integration.ts b/integrations/scraping-cuenca-mino-sil/src/integration.ts index a5328ef..d997214 100644 --- a/integrations/scraping-cuenca-mino-sil/src/integration.ts +++ b/integrations/scraping-cuenca-mino-sil/src/integration.ts @@ -9,6 +9,5 @@ export async function scrapeCuencaMinioSil(): Promise const $: cheerio.CheerioAPI = cheerio.load(html); const rawEmbalses = extractProvinceTables($); - console.log("Embalses extraídos:", rawEmbalses); return mapToEmbalseUpdateSAIH(rawEmbalses); } diff --git a/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts b/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts index 89e34bf..135ddc9 100644 --- a/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts +++ b/integrations/scraping-cuenca-mino-sil/src/scraper/business.ts @@ -16,7 +16,7 @@ export function extractProvinceTables( $: cheerio.CheerioAPI ): EmbalsesMinoSil[] { - const embalses: EmbalsesMinoSil[] = []; + let embalses: EmbalsesMinoSil[] = []; $("table.tabla tr").each((_index, row) => { const cells = $(row).find("td"); @@ -29,13 +29,15 @@ export function extractProvinceTables( const fecha = $(cells[8]).text().trim(); const volumenActual = $(cells[6]).text().trim(); - embalses.push({ + const embalse: EmbalsesMinoSil = { id: Number(id), embalse: nombre.split(" - ")[1].trim(), capacidadTotalHm3: parseEuropeanNumber(capacidadTotal), volumenActualHm3: parseEuropeanNumber(volumenActual), fecha: mapStringToApiDate(fecha) - }); + }; + + embalses = [...embalses, embalse]; }); return embalses; diff --git a/integrations/scraping-cuenca-mino-sil/src/scraper/helpers.ts b/integrations/scraping-cuenca-mino-sil/src/scraper/helpers.ts index eb0dca2..206b950 100644 --- a/integrations/scraping-cuenca-mino-sil/src/scraper/helpers.ts +++ b/integrations/scraping-cuenca-mino-sil/src/scraper/helpers.ts @@ -1,5 +1,5 @@ export const mapStringToApiDate = (strDate: string) => { - const [year, month, day] = strDate.split(" ")[0].split('/'); + const [day, month, year] = strDate.split(" ")[0].split('/'); return `${day}/${month}/${year}`; } diff --git a/packages/db/src/dals/embalses/embalses.mappers.ts b/packages/db/src/dals/embalses/embalses.mappers.ts index 7035d10..7992ff4 100644 --- a/packages/db/src/dals/embalses/embalses.mappers.ts +++ b/packages/db/src/dals/embalses/embalses.mappers.ts @@ -214,3 +214,39 @@ export const mapperFromCuencasSeguraToArcgis = new Map< [4, { nombre: "Camarillas", idArcgis: 72 }], [5, { nombre: "La Pedrera", idArcgis: 180 }], ]); + +// Ojo AZUFRE, MONTEARENAS, GUISTOLAS + +export const mapperFromCuencasMinoSilToArcgis = new Map< + number, + InfoDestinoArcgis +>([ + [3, { nombre: "LAS ROZA", idArcgis: 262 }], + [5, { nombre: "MATALAVILLA", idArcgis: 198 }], + [7, { nombre: "BÁRCENA", idArcgis: 67 }], + [1, { nombre: "BELESAR", idArcgis: 45 }], + [2, { nombre: "OS PEARES", idArcgis: 221 }], + [13, { nombre: "SAN MARTIÑO", idArcgis: 278 }], + [23, { nombre: "MONTEFURADO", idArcgis: 205 }], + [27, { nombre: "SANTO ESTEVO", idArcgis: 275 }], + [28, { nombre: "VILASOUTO", idArcgis: 339 }], + [16, { nombre: "AS PORTAS", idArcgis: 234 }], + [18, { nombre: "BAO", idArcgis: 37 }], + [19, { nombre: "PRADA", idArcgis: 238 }], + [30, { nombre: "VELLE", idArcgis: 335 }], + [31, { nombre: "CASTRELO", idArcgis: 85 }], + [32, { nombre: "ALBARELLOS", idArcgis: 10 }], + [33, { nombre: "FRIEIRA", idArcgis: 142 }], + [35, { nombre: "AS CONCHAS", idArcgis: 107 }], + [36, { nombre: "SALAS", idArcgis: 269 }], + [39, { nombre: "LA CAMPAÑANA", idArcgis: 73 }], + [40, { nombre: "PEÑARRUBIA", idArcgis: 226 }], + [66, { nombre: "SEQUEIROS", idArcgis: 292 }], + [70, { nombre: "SAN PEDRO", idArcgis: 279 }], + [74, { nombre: "SAN SEBASTIÁN", idArcgis: 281 }], + [76, { nombre: "PÍAS", idArcgis: 246 }], + [77, { nombre: "CENZA", idArcgis: 95 }], + [82, { nombre: "SANTA EULALIA", idArcgis: 285 }], + [89, { nombre: "CHANDREXA", idArcgis: 99 }], + [97, { nombre: "EDRADA", idArcgis: 124 }], +]) diff --git a/packages/db/src/dals/embalses/embalses.repository.ts b/packages/db/src/dals/embalses/embalses.repository.ts index 0ef4a23..02edb89 100644 --- a/packages/db/src/dals/embalses/embalses.repository.ts +++ b/packages/db/src/dals/embalses/embalses.repository.ts @@ -1,6 +1,6 @@ import { scrapeSeedEmbalses } from "arcgis"; import { getEmbalsesContext } from "./embalses.context.js"; -import { mapperFromCuencasMediterraneaToArcgis, mapperFromCuencasCantabricoToArcgis, mapperFromCuencasCatalanaToArcgis, mapperFromCuencasDueroToArcgis, mapperFromCuencasJucarToArcgis, mapperFromCuencasSeguraToArcgis } from "./embalses.mappers.js"; +import { mapperFromCuencasMediterraneaToArcgis, mapperFromCuencasCantabricoToArcgis, mapperFromCuencasCatalanaToArcgis, mapperFromCuencasDueroToArcgis, mapperFromCuencasJucarToArcgis, mapperFromCuencasSeguraToArcgis, mapperFromCuencasMinoSilToArcgis } from "./embalses.mappers.js"; import { scrapeCuencaMediterranea } from "scraping-cuenca-mediterranea"; import { scrapeCuencaCantabrica } from 'scraping-cuenca-cantabrico'; import { integracionCuencaCatalana } from 'scraping-cuenca-catalana'; @@ -10,6 +10,7 @@ import { getEstadoCuencaDuero } from 'scraping-cuenca-duero'; // import { scrapeCuencaGuadalquivir } from 'scraping-cuenca-guadalquivir'; import { scrapeCuencaJucar } from 'scraping-cuenca-jucar'; import { scrapeCuencaSegura } from 'scraping-cuenca-segura'; +import { scrapeCuencaMinioSil } from 'scraping-cuenca-mino-sil'; import { parseDate } from "./embalses.helpers.js"; export const embalsesRepository = { @@ -340,6 +341,55 @@ export const embalsesRepository = { } } + return actualizados > 0; + }, + actualizarCuencaMinoSil: async (): Promise => { + const embalsesMinoSil = await scrapeCuencaMinioSil(); + + console.log( + `Se han scrapeado ${embalsesMinoSil.length} embalses de la Cuenca Mino Sil` + ); + + let actualizados = 0; + let noEncontrados = 0; + let sinMapper = 0; + + for (const embalse of embalsesMinoSil) { + const infoDestino = mapperFromCuencasMinoSilToArcgis.get(embalse.id); + + if (!infoDestino) { + sinMapper++; + console.warn(`Sin mapper para ID ${embalse.id} - ${embalse.nombre}`); + continue; + } + + console.log( + `🔍 Mapeando: ID scraping ${embalse.id} -> _id BD ${infoDestino.idArcgis} (${infoDestino.nombre})` + ); + + const { matchedCount } = await getEmbalsesContext().updateOne( + { _id: infoDestino.idArcgis.toString() }, + { + $set: { + aguaActualSAIH: embalse.aguaActualSAIH, + fechaMedidaAguaActualSAIH: parseDate(embalse.fechaMedidaSAIH), + }, + } + ); + + if (matchedCount > 0) { + actualizados++; + console.log( + `Actualizado: ${infoDestino.nombre} (_id: ${infoDestino.idArcgis}) -> ${embalse.aguaActualSAIH} hm³` + ); + } else { + noEncontrados++; + console.warn( + `No encontrado en BD: _id ${infoDestino.idArcgis} - ${infoDestino.nombre}` + ); + } + } + return actualizados > 0; } }; From d62ad570f76568f432c12473edf37fc16f04fffa Mon Sep 17 00:00:00 2001 From: Antonio Contreras Date: Thu, 26 Feb 2026 16:51:02 +0100 Subject: [PATCH 7/7] fix: update parseDate function to create date in UTC to avoid timezone discrepancies --- packages/db/src/dals/embalses/embalses.helpers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/db/src/dals/embalses/embalses.helpers.ts b/packages/db/src/dals/embalses/embalses.helpers.ts index b6e1d88..5b5a842 100644 --- a/packages/db/src/dals/embalses/embalses.helpers.ts +++ b/packages/db/src/dals/embalses/embalses.helpers.ts @@ -1,4 +1,5 @@ export const parseDate = (dateStr: string): Date => { const [day, month, year] = dateStr.split("/").map(Number); - return new Date(year, month - 1, day); + // Crea la fecha en UTC para evitar desfase por zona horaria + return new Date(Date.UTC(year, month - 1, day)); };