Keep workers online during long-running downloads

This commit is contained in:
erik
2026-09-04 14:53:37 -05:00
parent d9e2012e22
commit 632599b201
57 changed files with 8847 additions and 21 deletions
@@ -0,0 +1,310 @@
import fs from "node:fs/promises";
import path from "node:path";
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
const workspace = "C:/Users/erik1/Documents/IPSExtractor";
const sourceDir = path.join(workspace, ".codex_tmp_apartments");
const outputDir = path.join(workspace, "outputs", "apartamentos_rentpana_20260830");
const outputPath = path.join(outputDir, "apartamentos_visibles_rentpana.xlsx");
const pageSize = 50;
const pages = [];
for (let pageIndex = 0; pageIndex < 3; pageIndex += 1) {
const text = await fs.readFile(path.join(sourceDir, `page_${pageIndex}.json`), "utf8");
const rows = JSON.parse(text);
pages.push({ page: pageIndex + 1, offset: pageIndex * pageSize, rows });
}
const apartments = pages.flatMap((page) => page.rows);
if (apartments.length !== 101) throw new Error(`Se esperaban 101 registros y llegaron ${apartments.length}`);
const ids = apartments.map((row) => row.id);
if (new Set(ids).size !== apartments.length) throw new Error("Se detectaron IDs duplicados en la extracción");
const columns = Object.keys(apartments[0]);
const expectedColumns = 34;
if (columns.length !== expectedColumns) throw new Error(`Se esperaban ${expectedColumns} columnas y llegaron ${columns.length}`);
const normalize = (key, value) => {
if (value === null || value === undefined) return null;
if (key === "created_at" || key === "updated_at") {
const date = new Date(`${value}T00:00:00Z`);
return Number.isNaN(date.getTime()) ? String(value) : date;
}
if (Array.isArray(value)) return value.join(" | ");
if (typeof value === "object") return JSON.stringify(value);
return value;
};
const colLetter = (oneBased) => {
let n = oneBased;
let result = "";
while (n > 0) {
const remainder = (n - 1) % 26;
result = String.fromCharCode(65 + remainder) + result;
n = Math.floor((n - 1) / 26);
}
return result;
};
const workbook = Workbook.create();
const summary = workbook.worksheets.add("Resumen");
const dataSheet = workbook.worksheets.add("Apartamentos");
const photosSheet = workbook.worksheets.add("Fotos");
const paginationSheet = workbook.worksheets.add("Paginacion");
const navy = "#123047";
const teal = "#0F766E";
const cyan = "#DFF4F1";
const pale = "#F4F7F9";
const gold = "#E8B44F";
const white = "#FFFFFF";
const ink = "#1F2937";
const muted = "#60707D";
const border = "#D8E1E7";
for (const sheet of [summary, dataSheet, photosSheet, paginationSheet]) {
sheet.showGridLines = false;
}
// Hoja principal con todos los campos devueltos por la API.
const dataMatrix = [columns, ...apartments.map((row) => columns.map((key) => normalize(key, row[key])))];
const dataEndCol = colLetter(columns.length);
const dataEndRow = dataMatrix.length;
dataSheet.getRange(`A1:${dataEndCol}${dataEndRow}`).values = dataMatrix;
dataSheet.freezePanes.freezeRows(1);
dataSheet.freezePanes.freezeColumns(2);
const apartmentTable = dataSheet.tables.add(`A1:${dataEndCol}${dataEndRow}`, true, "ApartmentsTable");
apartmentTable.style = "TableStyleMedium2";
apartmentTable.showFilterButton = true;
dataSheet.getRange(`A1:${dataEndCol}1`).format = {
fill: navy,
font: { bold: true, color: white },
rowHeight: 28,
verticalAlignment: "center",
};
dataSheet.getRange(`A2:${dataEndCol}${dataEndRow}`).format = {
font: { color: ink, size: 10 },
verticalAlignment: "top",
};
const widths = {
id: 36, user_id: 36, user_name: 22, user_phone: 16, address: 34, description: 60,
bedrooms: 12, bathrooms: 12, area_m2: 12, monthly_rent: 16, available_rooms: 16,
building_amenities: 38, apartment_features: 44, included_services: 36, photo_urls: 65,
created_at: 14, updated_at: 14, show_phone: 12, show_profile: 13, is_visible: 12,
priority: 11, listing_type: 14, city: 17, area: 24, short_id: 13,
smart_rental_enabled: 20, parent_apartment_id: 36, tenant_user_id: 36,
tenant_name: 20, tenant_phone: 16, tenant_gender: 15, tenant_age: 12,
is_paid: 11, seo_slug: 38,
};
columns.forEach((key, index) => {
const letter = colLetter(index + 1);
dataSheet.getRange(`${letter}:${letter}`).format.columnWidth = widths[key] ?? 16;
});
for (const key of ["description", "building_amenities", "apartment_features", "included_services", "photo_urls"]) {
const index = columns.indexOf(key) + 1;
const letter = colLetter(index);
dataSheet.getRange(`${letter}2:${letter}${dataEndRow}`).format.wrapText = true;
}
dataSheet.getRange(`A2:${dataEndCol}${dataEndRow}`).format.rowHeight = 42;
for (const key of ["created_at", "updated_at"]) {
const letter = colLetter(columns.indexOf(key) + 1);
dataSheet.getRange(`${letter}2:${letter}${dataEndRow}`).format.numberFormat = "yyyy-mm-dd";
}
for (const key of ["bedrooms", "bathrooms", "area_m2", "available_rooms", "priority", "tenant_age"]) {
const letter = colLetter(columns.indexOf(key) + 1);
dataSheet.getRange(`${letter}2:${letter}${dataEndRow}`).format.numberFormat = "#,##0";
}
const rentLetter = colLetter(columns.indexOf("monthly_rent") + 1);
dataSheet.getRange(`${rentLetter}2:${rentLetter}${dataEndRow}`).format.numberFormat = '"$"#,##0';
// Una fila por foto para facilitar filtros, descarga y revisión.
const photoRows = [];
for (const apartment of apartments) {
const urls = Array.isArray(apartment.photo_urls) ? apartment.photo_urls : [];
urls.forEach((url, index) => photoRows.push([
apartment.id,
apartment.short_id,
apartment.city,
Array.isArray(apartment.area) ? apartment.area.join(" | ") : apartment.area,
apartment.address,
index + 1,
url,
]));
}
const photoMatrix = [["apartment_id", "short_id", "city", "area", "address", "photo_index", "photo_url"], ...photoRows];
photosSheet.getRange(`A1:G${photoMatrix.length}`).values = photoMatrix;
photosSheet.freezePanes.freezeRows(1);
photosSheet.freezePanes.freezeColumns(2);
const photosTable = photosSheet.tables.add(`A1:G${photoMatrix.length}`, true, "PhotosTable");
photosTable.style = "TableStyleMedium2";
photosTable.showFilterButton = true;
photosSheet.getRange("A1:G1").format = { fill: navy, font: { bold: true, color: white }, rowHeight: 28 };
photosSheet.getRange(`A2:G${photoMatrix.length}`).format = { font: { color: ink, size: 10 }, rowHeight: 22 };
photosSheet.getRange("A:A").format.columnWidth = 36;
photosSheet.getRange("B:B").format.columnWidth = 13;
photosSheet.getRange("C:C").format.columnWidth = 16;
photosSheet.getRange("D:D").format.columnWidth = 24;
photosSheet.getRange("E:E").format.columnWidth = 36;
photosSheet.getRange("F:F").format.columnWidth = 12;
photosSheet.getRange("G:G").format.columnWidth = 90;
photosSheet.getRange(`F2:F${photoMatrix.length}`).format.numberFormat = "#,##0";
// Control detallado de las tres páginas consultadas.
const paginationRows = pages.map((page) => {
const from = page.offset;
const to = page.offset + page.rows.length - 1;
return [
page.page,
page.offset,
pageSize,
`${from}-${to}`,
page.rows.length,
"200 OK",
page.rows[0]?.id ?? null,
page.rows.at(-1)?.id ?? null,
];
});
const paginationMatrix = [["pagina", "offset", "limite", "rango_respuesta", "filas", "estado_http", "primer_id", "ultimo_id"], ...paginationRows];
paginationSheet.getRange("A1:H4").values = paginationMatrix;
paginationSheet.getRange("A5:D5").merge();
paginationSheet.getRange("A5:D5").values = [["Total descargado"]];
paginationSheet.getRange("E5").formulas = [["=SUM(E2:E4)"]];
paginationSheet.getRange("F5:H5").merge();
paginationSheet.getRange("F5:H5").values = [["Validado contra Content-Range: 101"]];
paginationSheet.getRange("A1:H1").format = { fill: navy, font: { bold: true, color: white }, rowHeight: 28 };
paginationSheet.getRange("A2:H4").format = { fill: white, font: { color: ink }, rowHeight: 24 };
paginationSheet.getRange("A5:H5").format = { fill: cyan, font: { bold: true, color: navy }, rowHeight: 26 };
paginationSheet.getRange("A1:H5").format.borders = { preset: "all", style: "thin", color: border };
paginationSheet.getRange("A2:F5").format.horizontalAlignment = "center";
paginationSheet.getRange("A:C").format.columnWidth = 12;
paginationSheet.getRange("D:D").format.columnWidth = 20;
paginationSheet.getRange("E:E").format.columnWidth = 12;
paginationSheet.getRange("F:F").format.columnWidth = 16;
paginationSheet.getRange("G:H").format.columnWidth = 38;
paginationSheet.getRange("A2:C5").format.numberFormat = "#,##0";
paginationSheet.freezePanes.freezeRows(1);
// Resumen ejecutivo y trazabilidad de la extracción.
summary.getRange("A1:F2").merge();
summary.getRange("A1:F2").values = [["Apartamentos visibles · RentPana"]];
summary.getRange("A1:F2").format = {
fill: navy,
font: { bold: true, color: white, size: 20 },
horizontalAlignment: "left",
verticalAlignment: "center",
};
summary.getRange("A3:F3").merge();
summary.getRange("A3:F3").values = [["Extracción completa, paginada y validada contra el total informado por la API"]];
summary.getRange("A3:F3").format = { fill: navy, font: { color: "#D9EDF2", size: 11 }, rowHeight: 24 };
summary.getRange("A5:B5").merge();
summary.getRange("A5:B5").values = [["Apartamentos"]];
summary.getRange("C5:D5").merge();
summary.getRange("C5:D5").values = [["Páginas API"]];
summary.getRange("E5:F5").merge();
summary.getRange("E5:F5").values = [["Fotos"]];
summary.getRange("A6:B7").merge();
summary.getRange("A6:B7").formulas = [["=COUNTA('Apartamentos'!$A$2:$A$102)"]];
summary.getRange("C6:D7").merge();
summary.getRange("C6:D7").formulas = [["=COUNTA('Paginacion'!$A$2:$A$4)"]];
summary.getRange("E6:F7").merge();
summary.getRange("E6:F7").formulas = [[`=COUNTA('Fotos'!$A$2:$A$${photoMatrix.length})`]];
summary.getRange("A5:F5").format = { fill: teal, font: { bold: true, color: white }, horizontalAlignment: "center", rowHeight: 24 };
summary.getRange("A6:F7").format = { fill: cyan, font: { bold: true, color: navy, size: 20 }, horizontalAlignment: "center", verticalAlignment: "center" };
summary.getRange("A5:F7").format.borders = { preset: "outside", style: "thin", color: border };
summary.getRange("A9:F9").merge();
summary.getRange("A9:F9").values = [["Control de extracción"]];
summary.getRange("A9:F9").format = { fill: teal, font: { bold: true, color: white }, rowHeight: 26 };
const extractedAt = new Date();
const controlRows = [
["Fecha y hora (UTC)", extractedAt, null, null, null, null],
["Origen", "https://srv99857.rentpana.com/rest/v1/apartments", null, null, null, null],
["Filtro", "is_visible = true", null, null, null, null],
["Orden estable", "id ascendente", null, null, null, null],
["Tamaño de página", pageSize, null, null, null, null],
["Total informado por API", 101, null, null, null, null],
["IDs únicos validados", new Set(ids).size, null, null, null, null],
["Duplicados detectados", apartments.length - new Set(ids).size, null, null, null, null],
["Columnas de origen", columns.length, null, null, null, null],
["Credenciales incluidas", "No", null, null, null, null],
];
summary.getRange("A10:F19").values = controlRows;
for (let row = 10; row <= 19; row += 1) summary.getRange(`B${row}:F${row}`).merge();
summary.getRange("A10:A19").format = { fill: pale, font: { bold: true, color: muted }, rowHeight: 24 };
summary.getRange("B10:F19").format = { fill: white, font: { color: ink }, rowHeight: 24, wrapText: true, horizontalAlignment: "left" };
summary.getRange("A10:F19").format.borders = { preset: "outside", style: "thin", color: border };
summary.getRange("B10").format.numberFormat = "yyyy-mm-dd hh:mm:ss";
summary.getRange("B14:B18").format.numberFormat = "#,##0";
summary.getRange("A21:F21").merge();
summary.getRange("A21:F21").values = [["Contenido del archivo"]];
summary.getRange("A21:F21").format = { fill: teal, font: { bold: true, color: white }, rowHeight: 26 };
const contentsRows = [
["Apartamentos", "Todos los 34 campos de origen; filtros activados y encabezado congelado."],
["Fotos", "Una fila por URL de foto, vinculada por apartment_id y short_id."],
["Paginacion", "Offset, límite, rango, filas, estado y IDs extremos por página."],
];
summary.getRange("A22:F24").values = contentsRows.map(([a, b]) => [a, b, null, null, null, null]);
for (let row = 22; row <= 24; row += 1) summary.getRange(`B${row}:F${row}`).merge();
summary.getRange("A22:A24").format = { fill: pale, font: { bold: true, color: navy }, rowHeight: 28 };
summary.getRange("B22:F24").format = { fill: white, font: { color: ink }, wrapText: true, rowHeight: 28 };
summary.getRange("A22:F24").format.borders = { preset: "outside", style: "thin", color: border };
summary.getRange("A:A").format.columnWidth = 24;
summary.getRange("B:F").format.columnWidth = 18;
summary.getRange("A1:F24").format.font.name = "Aptos";
summary.freezePanes.freezeRows(3);
await fs.mkdir(outputDir, { recursive: true });
const checks = [];
checks.push((await workbook.inspect({
kind: "table",
range: "Resumen!A1:F24",
include: "values,formulas",
tableMaxRows: 24,
tableMaxCols: 6,
maxChars: 6000,
})).ndjson);
checks.push((await workbook.inspect({
kind: "table",
range: "Paginacion!A1:H5",
include: "values,formulas",
tableMaxRows: 8,
tableMaxCols: 8,
maxChars: 3000,
})).ndjson);
checks.push((await workbook.inspect({
kind: "match",
searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A",
options: { useRegex: true, maxResults: 300 },
summary: "final formula error scan",
})).ndjson);
const previews = [
["Resumen", "A1:F24", "preview_resumen.png"],
["Apartamentos", "A1:J10", "preview_apartamentos_1.png"],
["Apartamentos", "K1:AH8", "preview_apartamentos_2.png"],
["Fotos", "A1:G12", "preview_fotos.png"],
["Paginacion", "A1:H5", "preview_paginacion.png"],
];
for (const [sheetName, range, fileName] of previews) {
const blob = await workbook.render({ sheetName, range, scale: 1.4, format: "png" });
await fs.writeFile(path.join(outputDir, fileName), new Uint8Array(await blob.arrayBuffer()));
}
const output = await SpreadsheetFile.exportXlsx(workbook);
await output.save(outputPath);
console.log(JSON.stringify({
outputPath,
apartments: apartments.length,
uniqueIds: new Set(ids).size,
photos: photoRows.length,
pages: pages.length,
columns: columns.length,
checks,
previews: previews.map((item) => path.join(outputDir, item[2])),
}, null, 2));
+22
View File
@@ -0,0 +1,22 @@
HTTP/1.1 206 Partial Content
Date: Sun, 30 Aug 2026 19:12:16 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 108239
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: 0-49/101
Content-Location: /apartments?is_visible=eq.true&limit=50&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 12
X-Kong-Proxy-Latency: 1
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 36d60653723f406a162c6535d728d27d
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=e%2FhgsmNUvfdXbxPGFrz%2FMjaXlkCiuYPlrQQ42rkkNFx4x%2BMdhLL3ImSLwz7rtCdYR1DHKjSIzEPLp4dHKjL0cypB8Z46vlG%2FHPjsRaBFF1ycsDO3VL4I73MtA5QRjQShWznGiKw38Qw%3D"}]}
CF-RAY: a3361b23bd70dfba-MIA
alt-svc: h3=":443"; ma=86400
+22
View File
@@ -0,0 +1,22 @@
HTTP/1.1 206 Partial Content
Date: Sun, 30 Aug 2026 19:12:16 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 110785
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: 50-99/101
Content-Location: /apartments?is_visible=eq.true&limit=50&offset=50&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 18
X-Kong-Proxy-Latency: 0
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 43005d3aa4dfd17006586b41c9903306
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=OXpuhJ8kF%2FzhCWI3QCdCE6R6ZZ33yEpgyPCVOIjJeIe4SC6LK%2BZp0Hoa%2FyYXkm49AUgc9W3NEB3RodHaby%2B3W8lVVfVUhYFlw392LbBZSINbEaP1olAo7Gd5CLHEl9fMltUYK%2F3FuZg%3D"}]}
CF-RAY: a3361b26a99d8dc6-MIA
alt-svc: h3=":443"; ma=86400
+22
View File
@@ -0,0 +1,22 @@
HTTP/1.1 206 Partial Content
Date: Sun, 30 Aug 2026 19:12:16 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2399
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: 100-100/101
Content-Location: /apartments?is_visible=eq.true&limit=50&offset=100&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 11
X-Kong-Proxy-Latency: 0
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: e5b9ec1e2e2ea72419eb096189826631
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=hs0K9bWUzpaTwFm4%2BZ2bo3c5Km2mVBEkNxHpE8aghdPj%2FqrUGgznk0IlCooLvaGKoFkvTUiyMIJq7CUi2tBqRFq%2B8XCjAsLBo3%2FbYXWL2lNJtjA8qv%2Fs0j5tFTYBGGd3RwD7dTbRWcw%3D"}]}
CF-RAY: a3361b29ada95c66-MIA
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,181 @@
import fs from "node:fs/promises";
import path from "node:path";
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
const workspace = "C:/Users/erik1/Documents/IPSExtractor";
const visibleSample = JSON.parse(await fs.readFile(path.join(workspace, ".codex_tmp_apartments", "page_0.json"), "utf8"));
const columns = Object.keys(visibleSample[0]);
const outputDir = path.join(workspace, "outputs", "apartamentos_no_visibles_rentpana_20260830");
const outputPath = path.join(outputDir, "apartamentos_no_visibles_rentpana.xlsx");
const colLetter = (oneBased) => {
let n = oneBased;
let result = "";
while (n > 0) {
const remainder = (n - 1) % 26;
result = String.fromCharCode(65 + remainder) + result;
n = Math.floor((n - 1) / 26);
}
return result;
};
const workbook = Workbook.create();
const summary = workbook.worksheets.add("Resumen");
const dataSheet = workbook.worksheets.add("Apartamentos");
const photosSheet = workbook.worksheets.add("Fotos");
const paginationSheet = workbook.worksheets.add("Paginacion");
const navy = "#123047";
const teal = "#0F766E";
const cyan = "#DFF4F1";
const pale = "#F4F7F9";
const white = "#FFFFFF";
const ink = "#1F2937";
const muted = "#60707D";
const border = "#D8E1E7";
for (const sheet of [summary, dataSheet, photosSheet, paginationSheet]) sheet.showGridLines = false;
const endCol = colLetter(columns.length);
dataSheet.getRange(`A1:${endCol}2`).values = [columns, columns.map(() => null)];
dataSheet.freezePanes.freezeRows(1);
dataSheet.freezePanes.freezeColumns(2);
const apartmentTable = dataSheet.tables.add(`A1:${endCol}2`, true, "HiddenApartmentsTable");
apartmentTable.style = "TableStyleMedium2";
apartmentTable.showFilterButton = true;
dataSheet.getRange(`A1:${endCol}1`).format = { fill: navy, font: { bold: true, color: white }, rowHeight: 28 };
dataSheet.getRange(`A2:${endCol}2`).format = { fill: white, rowHeight: 24 };
const widths = {
id: 36, user_id: 36, user_name: 22, user_phone: 16, address: 34, description: 60,
bedrooms: 12, bathrooms: 12, area_m2: 12, monthly_rent: 16, available_rooms: 16,
building_amenities: 38, apartment_features: 44, included_services: 36, photo_urls: 65,
created_at: 14, updated_at: 14, show_phone: 12, show_profile: 13, is_visible: 12,
priority: 11, listing_type: 14, city: 17, area: 24, short_id: 13,
smart_rental_enabled: 20, parent_apartment_id: 36, tenant_user_id: 36,
tenant_name: 20, tenant_phone: 16, tenant_gender: 15, tenant_age: 12,
is_paid: 11, seo_slug: 38,
};
columns.forEach((key, index) => dataSheet.getRange(`${colLetter(index + 1)}:${colLetter(index + 1)}`).format.columnWidth = widths[key] ?? 16);
const photoHeaders = ["apartment_id", "short_id", "city", "area", "address", "photo_index", "photo_url"];
photosSheet.getRange("A1:G2").values = [photoHeaders, photoHeaders.map(() => null)];
photosSheet.freezePanes.freezeRows(1);
photosSheet.freezePanes.freezeColumns(2);
const photosTable = photosSheet.tables.add("A1:G2", true, "HiddenPhotosTable");
photosTable.style = "TableStyleMedium2";
photosTable.showFilterButton = true;
photosSheet.getRange("A1:G1").format = { fill: navy, font: { bold: true, color: white }, rowHeight: 28 };
photosSheet.getRange("A:A").format.columnWidth = 36;
photosSheet.getRange("B:B").format.columnWidth = 13;
photosSheet.getRange("C:C").format.columnWidth = 16;
photosSheet.getRange("D:D").format.columnWidth = 24;
photosSheet.getRange("E:E").format.columnWidth = 36;
photosSheet.getRange("F:F").format.columnWidth = 12;
photosSheet.getRange("G:G").format.columnWidth = 90;
paginationSheet.getRange("A1:H2").values = [
["pagina", "offset", "limite", "content_range", "filas", "estado_http", "primer_id", "ultimo_id"],
[1, 0, 50, "*/0", 0, "200 OK", null, null],
];
paginationSheet.getRange("A3:D3").merge();
paginationSheet.getRange("A3:D3").values = [["Total descargado"]];
paginationSheet.getRange("E3").formulas = [["=SUM(E2:E2)"]];
paginationSheet.getRange("F3:H3").merge();
paginationSheet.getRange("F3:H3").values = [["Validado contra Content-Range: */0"]];
paginationSheet.getRange("A1:H1").format = { fill: navy, font: { bold: true, color: white }, rowHeight: 28 };
paginationSheet.getRange("A2:H2").format = { fill: white, font: { color: ink }, rowHeight: 24 };
paginationSheet.getRange("A3:H3").format = { fill: cyan, font: { bold: true, color: navy }, rowHeight: 26 };
paginationSheet.getRange("A1:H3").format.borders = { preset: "all", style: "thin", color: border };
paginationSheet.getRange("A2:F3").format.horizontalAlignment = "center";
paginationSheet.getRange("A:C").format.columnWidth = 12;
paginationSheet.getRange("D:D").format.columnWidth = 20;
paginationSheet.getRange("E:E").format.columnWidth = 12;
paginationSheet.getRange("F:F").format.columnWidth = 16;
paginationSheet.getRange("G:H").format.columnWidth = 38;
paginationSheet.freezePanes.freezeRows(1);
summary.getRange("A1:F2").merge();
summary.getRange("A1:F2").values = [["Apartamentos no visibles · RentPana"]];
summary.getRange("A1:F2").format = { fill: navy, font: { bold: true, color: white, size: 20 }, verticalAlignment: "center" };
summary.getRange("A3:F3").merge();
summary.getRange("A3:F3").values = [["Consulta independiente con is_visible = false"]];
summary.getRange("A3:F3").format = { fill: navy, font: { color: "#D9EDF2", size: 11 }, rowHeight: 24 };
summary.getRange("A5:B5").merge();
summary.getRange("C5:D5").merge();
summary.getRange("E5:F5").merge();
summary.getRange("A5:F5").values = [["Apartamentos", null, "Páginas consultadas", null, "Fotos", null]];
summary.getRange("A6:B7").merge();
summary.getRange("A6:B7").formulas = [["=COUNTA('Apartamentos'!$A$2:$A$2)"]];
summary.getRange("C6:D7").merge();
summary.getRange("C6:D7").formulas = [["=COUNTA('Paginacion'!$A$2:$A$2)"]];
summary.getRange("E6:F7").merge();
summary.getRange("E6:F7").formulas = [["=COUNTA('Fotos'!$A$2:$A$2)"]];
summary.getRange("A5:F5").format = { fill: teal, font: { bold: true, color: white }, horizontalAlignment: "center", rowHeight: 24 };
summary.getRange("A6:F7").format = { fill: cyan, font: { bold: true, color: navy, size: 20 }, horizontalAlignment: "center", verticalAlignment: "center" };
summary.getRange("A5:F7").format.borders = { preset: "outside", style: "thin", color: border };
summary.getRange("A9:F9").merge();
summary.getRange("A9:F9").values = [["Control de extracción"]];
summary.getRange("A9:F9").format = { fill: teal, font: { bold: true, color: white }, rowHeight: 26 };
const controlRows = [
["Fecha y hora (UTC)", new Date()],
["Origen", "https://srv99857.rentpana.com/rest/v1/apartments"],
["Filtro", "is_visible = false"],
["Orden estable", "id ascendente"],
["Tamaño de página", 50],
["Total informado por API", 0],
["IDs únicos validados", 0],
["Duplicados detectados", 0],
["Columnas de origen", columns.length],
["Resultado", "La API devolvió 0 registros para este filtro con la sesión suministrada."],
["Credenciales incluidas", "No"],
];
summary.getRange("A10:F20").values = controlRows.map(([a, b]) => [a, b, null, null, null, null]);
for (let row = 10; row <= 20; row += 1) summary.getRange(`B${row}:F${row}`).merge();
summary.getRange("A10:A20").format = { fill: pale, font: { bold: true, color: muted }, rowHeight: 24 };
summary.getRange("B10:F20").format = { fill: white, font: { color: ink }, rowHeight: 24, wrapText: true, horizontalAlignment: "left" };
summary.getRange("A10:F20").format.borders = { preset: "outside", style: "thin", color: border };
summary.getRange("B10").format.numberFormat = "yyyy-mm-dd hh:mm:ss";
summary.getRange("A22:F22").merge();
summary.getRange("A22:F22").values = [["Contenido del archivo"]];
summary.getRange("A22:F22").format = { fill: teal, font: { bold: true, color: white }, rowHeight: 26 };
const contents = [
["Apartamentos", "Estructura de 34 campos, lista para los registros devueltos por la consulta."],
["Fotos", "Hoja normalizada para enlaces de fotos; actualmente sin filas."],
["Paginacion", "Consulta, límite, rango, cantidad y estado HTTP documentados."],
];
summary.getRange("A23:F25").values = contents.map(([a, b]) => [a, b, null, null, null, null]);
for (let row = 23; row <= 25; row += 1) summary.getRange(`B${row}:F${row}`).merge();
summary.getRange("A23:A25").format = { fill: pale, font: { bold: true, color: navy }, rowHeight: 28 };
summary.getRange("B23:F25").format = { fill: white, font: { color: ink }, wrapText: true, rowHeight: 28 };
summary.getRange("A23:F25").format.borders = { preset: "outside", style: "thin", color: border };
summary.getRange("A:A").format.columnWidth = 24;
summary.getRange("B:F").format.columnWidth = 18;
summary.getRange("A1:F25").format.font.name = "Aptos";
summary.freezePanes.freezeRows(3);
await fs.mkdir(outputDir, { recursive: true });
const checks = [
(await workbook.inspect({ kind: "table", range: "Resumen!A1:F25", include: "values,formulas", tableMaxRows: 25, tableMaxCols: 6, maxChars: 6000 })).ndjson,
(await workbook.inspect({ kind: "table", range: "Paginacion!A1:H3", include: "values,formulas", tableMaxRows: 5, tableMaxCols: 8, maxChars: 2500 })).ndjson,
(await workbook.inspect({ kind: "match", searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A", options: { useRegex: true, maxResults: 300 }, summary: "final formula error scan" })).ndjson,
];
const previews = [
["Resumen", "A1:F25", "preview_resumen.png"],
["Apartamentos", "A1:J2", "preview_apartamentos_1.png"],
["Apartamentos", "K1:AH2", "preview_apartamentos_2.png"],
["Fotos", "A1:G2", "preview_fotos.png"],
["Paginacion", "A1:H3", "preview_paginacion.png"],
];
for (const [sheetName, range, fileName] of previews) {
const blob = await workbook.render({ sheetName, range, scale: 1.4, format: "png" });
await fs.writeFile(path.join(outputDir, fileName), new Uint8Array(await blob.arrayBuffer()));
}
const output = await SpreadsheetFile.exportXlsx(workbook);
await output.save(outputPath);
console.log(JSON.stringify({ outputPath, rows: 0, pagesConsulted: 1, columns: columns.length, checks, previews: previews.map((item) => path.join(outputDir, item[2])) }, null, 2));
@@ -0,0 +1,22 @@
HTTP/1.1 200 OK
Date: Sun, 30 Aug 2026 19:58:23 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: */0
Content-Location: /apartments?is_visible=not.is.true&limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 4
X-Kong-Proxy-Latency: 0
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: ec6314205fccca4904d6d03407154c4a
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=8AohAv0A3ziLB3FtLIOhjzFdv85QxTqb1tAHIbT9ZaS8Azt0wu0aaZ%2FglV2Q18Z5XL%2F4QWsmSasuKWaPwKMMu7fkj6Ms1inrSl5uPbnKqH6JcOG66Cw%2FTMgdpHxGmOxLFIoMxU5F%2BdY%3D"}]}
CF-RAY: a3365eb52dfced7d-MIA
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,22 @@
HTTP/1.1 200 OK
Date: Sun, 30 Aug 2026 19:58:22 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: */0
Content-Location: /apartments?is_visible=eq.false&limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 7
X-Kong-Proxy-Latency: 1
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: fd33fa311539833f4a5ff50e90483a1a
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=NVKvV0awlCCGFLEGDS6qtWWoE1OawW5qkTusVN7UPmcUUz9ANiLsKREPkBGGlVgXtHV%2BhGfN2HE5KLVyYayaXmhi12828xkTJfNHfgC8loe5z3oA45mHEkThiTqq4qeMFFBk0z%2FF3jU%3D"}]}
CF-RAY: a3365eb14b4920ce-MIA
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,22 @@
HTTP/1.1 200 OK
Date: Sun, 30 Aug 2026 19:58:23 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: */0
Content-Location: /apartments?is_visible=is.false&limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 7
X-Kong-Proxy-Latency: 3
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 788604537f4afd42a27dc7376de1cc0d
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=DBm2LKUWUatJi1DLvGFP6TkUGCAMtPwejSduHaNrdK9LQpD%2BUydLT5bIhe8CGGf6bMizlPvFkMbqIsb1pT4FZP2mGjvL570cJpUe%2BroGEy6Tw0RmL%2Bf4f7L4zZ1RGc8sH6wuBA29GMw%3D"}]}
CF-RAY: a3365eb33e410cd1-MIA
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,22 @@
HTTP/1.1 206 Partial Content
Date: Sun, 30 Aug 2026 19:58:22 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2648
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: 0-0/101
Content-Location: /apartments?limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 12
X-Kong-Proxy-Latency: 1
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 1a3b2b0fc9128a7fa1ec2840f5635327
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=vgZDSTap%2BSWUr7NuXZGQIE5Rn48TAu8OqXveTvdG3%2F9IkXH1NAXG%2Fb%2F9zcO6pXtFdmaBDsQyN%2F40uUjtH0SwwmDE1Pzrp2dR3CiiBqds5HJFAb9M8mngLuZqb330Fg3xhsv3VjGysLA%3D"}]}
CF-RAY: a3365ead3d723405-MIA
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,22 @@
HTTP/1.1 206 Partial Content
Date: Sun, 30 Aug 2026 19:58:22 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2648
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: 0-0/101
Content-Location: /apartments?is_visible=eq.true&limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 4
X-Kong-Proxy-Latency: 1
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 9b533b383646b2728617ef2a30e7d92f
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=%2BGmFgkGsDol59lrAXlrXdP4PeE7tfE4zwIcSf7NgeVi1KjsaibLlq%2B%2FPHaZ%2FQSeodYa5%2BQsYoh1nULeLCJU9%2FzkRS3wsXDrF1K5jDwyiS1fOKMcM%2Fl%2B%2F5k84tjozth%2B%2FMgZ6mhogkFo%3D"}]}
CF-RAY: a3365eaf4e4e7bf9-MIA
alt-svc: h3=":443"; ma=86400
+22
View File
@@ -0,0 +1,22 @@
HTTP/1.1 206 Partial Content
Date: Sun, 30 Aug 2026 19:39:31 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2648
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: 0-0/101
Content-Location: /apartments?limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 45
X-Kong-Proxy-Latency: 1
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 039e86122c46200cd717916b6e135842
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=BKv08O8bUUr4qDMLzEOxeINGc5VFoXdVQNNDhmT8Lc%2BM%2ByjJsGfjnkOfLuHtkIBeBsGh4kmOPlN%2FB3s2If%2BcmCNw6oZLjYg8bPKo1QNCO0AmTmYXSNoT5e7%2BzJochHeSxCsD1gW7m38%3D"}]}
CF-RAY: a3364312bcedb887-MIA
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,22 @@
HTTP/1.1 200 OK
Date: Sun, 30 Aug 2026 19:39:32 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: */0
Content-Location: /apartments?is_visible=is.false&limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 30
X-Kong-Proxy-Latency: 2
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 9c2a57070a4b53249c28ac4c437d4b56
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=mAw%2BHylzVDcPCkK%2BIYK9f8zB9WEkfty7bMwyvjbyO%2B2IRzTN54jQAgofMSBrJOJOX6lxDDMScXE6iWe59XKESXlSDxYhmGLT%2FdwAHTAErXqgnYHCDtgCOh5GliHuDLBpf3o5zL8JJJE%3D"}]}
CF-RAY: a3364315582aeeeb-MIA
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,22 @@
HTTP/1.1 200 OK
Date: Sun, 30 Aug 2026 19:39:31 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: */0
Content-Location: /apartments?is_visible=not.is.true&limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 38
X-Kong-Proxy-Latency: 3
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 252f2d0642d8464a133408069dfd8a5a
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=JhblutXOican2RX5cbcS8caKnc6WpdsodPvtjWOzwAml8Yomg3xKSs8m2aCpXaXkTzM7Jka8X1htOJh3Zj5uoGMPrpnsHEgm6tWSnKdytSugJz2OePeD8PoHh6m7%2B%2FPMXKhXEJwmcjQ%3D"}]}
CF-RAY: a33643102b0e4fad-MIA
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,22 @@
HTTP/1.1 200 OK
Date: Sun, 30 Aug 2026 19:39:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
Connection: keep-alive
Server: cloudflare
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Content-Range: */0
Content-Location: /apartments?is_visible=eq.false&limit=1&offset=0&order=id.asc&select=%2A
Content-Profile: public
Preference-Applied: count=exact
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Encoding, Content-Location, Content-Range, Content-Type, Date, Location, Server, Transfer-Encoding, Range-Unit
X-Kong-Upstream-Latency: 28
X-Kong-Proxy-Latency: 2
Via: 1.1 kong/3.9.1
X-Kong-Request-Id: 6d6ae32336046505b27faf4e2bca1a3f
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=kgZGlNNkVBjD7LKeaIEHFnIeOOcPvmxhdN%2FN4gUHg3RSwToezpIG%2B0CTGdk%2FFozCIHhg34IuG92GUhUHgI3iwQdgyEouvC1744S1aZVWHXicpXdcY6OJyfB%2BqGA2lza4%2B%2FVGt1jIW%2Fo%3D"}]}
CF-RAY: a336424d59939aa6-MIA
alt-svc: h3=":443"; ma=86400
Binary file not shown.
+2 -2
View File
@@ -2,8 +2,8 @@
BOT_API_TOKEN=dev-token-change-me BOT_API_TOKEN=dev-token-change-me
BOT_API_URL=https://pys1.builker.xyz/bot-descarga BOT_API_URL=https://pys1.builker.xyz/bot-descarga
# Imaging (hostname interno; requiere VPN/DNS o IP alcanzable) # Imaging (requiere acceso a la red interna de Salud Total)
BOT_IMAGING_URL=http://serviciosimaging:8180/ BOT_IMAGING_URL=https://serviciosimaging.saludtotal.loc/
# MinIO — bucket pys-cuentas-medicas: sin credenciales (PutObject anónimo) # MinIO — bucket pys-cuentas-medicas: sin credenciales (PutObject anónimo)
MINIO_ENDPOINT=https://s3.builker.xyz MINIO_ENDPOINT=https://s3.builker.xyz
@@ -7,7 +7,7 @@ from pathlib import Path
@dataclass(frozen=True) @dataclass(frozen=True)
class Settings: class Settings:
url_sistema: str = "http://serviciosimaging:8180/" url_sistema: str = "https://serviciosimaging.saludtotal.loc/"
peso_minimo_bytes: int = 10_000 peso_minimo_bytes: int = 10_000
max_intentos_click: int = 3 max_intentos_click: int = 3
max_intentos_pdf: int = 2 max_intentos_pdf: int = 2
+47 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import socket import socket
import tempfile import tempfile
import threading
import time import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@@ -15,6 +16,9 @@ JOB_COMPLETED = "completed"
JOB_FAILED = "failed" JOB_FAILED = "failed"
JOB_CANCELLED = "cancelled" JOB_CANCELLED = "cancelled"
CONTROL_REQUEST_TIMEOUT_SECONDS = 5.0
ACTIVE_JOB_HEARTBEAT_SECONDS = 10.0
class RemoteApiClient: class RemoteApiClient:
"""Cliente HTTP hacia la API en IPSExtractor (vía gateway). Sin puertos locales.""" """Cliente HTTP hacia la API en IPSExtractor (vía gateway). Sin puertos locales."""
@@ -49,6 +53,7 @@ class RemoteApiClient:
r = self._client.post( r = self._client.post(
self._url("/workers/heartbeat"), self._url("/workers/heartbeat"),
json={"worker_id": worker_id}, json={"worker_id": worker_id},
timeout=CONTROL_REQUEST_TIMEOUT_SECONDS,
) )
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@@ -70,11 +75,18 @@ class RemoteApiClient:
return dest return dest
def post_event(self, job_id: str, event: Dict[str, Any]) -> None: def post_event(self, job_id: str, event: Dict[str, Any]) -> None:
r = self._client.post(self._url(f"/jobs/{job_id}/events"), json=event) r = self._client.post(
self._url(f"/jobs/{job_id}/events"),
json=event,
timeout=CONTROL_REQUEST_TIMEOUT_SECONDS,
)
r.raise_for_status() r.raise_for_status()
def cancel_flag(self, job_id: str) -> bool: def cancel_flag(self, job_id: str) -> bool:
r = self._client.get(self._url(f"/jobs/{job_id}/cancel-flag")) r = self._client.get(
self._url(f"/jobs/{job_id}/cancel-flag"),
timeout=CONTROL_REQUEST_TIMEOUT_SECONDS,
)
r.raise_for_status() r.raise_for_status()
return bool(r.json().get("cancel_requested")) return bool(r.json().get("cancel_requested"))
@@ -93,12 +105,14 @@ class WorkerAgent:
worker_id: str, worker_id: str,
token: str, token: str,
poll_seconds: float = 8.0, poll_seconds: float = 8.0,
heartbeat_seconds: float = ACTIVE_JOB_HEARTBEAT_SECONDS,
staging_root: Optional[Path] = None, staging_root: Optional[Path] = None,
log_func=None, log_func=None,
): ):
self.api = RemoteApiClient(api_url, token) self.api = RemoteApiClient(api_url, token)
self.worker_id = worker_id self.worker_id = worker_id
self.poll_seconds = poll_seconds self.poll_seconds = poll_seconds
self.heartbeat_seconds = max(1.0, float(heartbeat_seconds))
self.staging_root = staging_root or Path(tempfile.gettempdir()) / "bot_descarga" self.staging_root = staging_root or Path(tempfile.gettempdir()) / "bot_descarga"
self.minio = WorkerMinioUploader(MinioSettings.from_env()) self.minio = WorkerMinioUploader(MinioSettings.from_env())
self._running = True self._running = True
@@ -129,6 +143,25 @@ class WorkerAgent:
self.api.close() self.api.close()
self._log(f"[worker {self.worker_id}] detenido") self._log(f"[worker {self.worker_id}] detenido")
def _heartbeat_while_job_runs(self, stop_event: threading.Event) -> None:
"""Mantiene al worker online mientras el procesamiento bloqueante continúa."""
connection_failed = False
while not stop_event.is_set():
try:
self.api.heartbeat(self.worker_id)
if connection_failed:
self._log(f"[worker {self.worker_id}] conexión con servidor restablecida")
connection_failed = False
except Exception as e:
# Una caída temporal del gateway no debe cancelar la descarga local.
if not connection_failed:
self._log(
f"[worker {self.worker_id}] heartbeat temporalmente no disponible: {e}; "
"el job continúa"
)
connection_failed = True
stop_event.wait(self.heartbeat_seconds)
def _run_job(self, job: dict) -> None: def _run_job(self, job: dict) -> None:
job_id = job["id"] job_id = job["id"]
downloads_prefix = (job.get("downloads_prefix") or "").strip() or f"downloads/{job_id}/" downloads_prefix = (job.get("downloads_prefix") or "").strip() or f"downloads/{job_id}/"
@@ -140,6 +173,15 @@ class WorkerAgent:
self._log(f"[worker {self.worker_id}] job {job_id} iniciado (prefix={downloads_prefix})") self._log(f"[worker {self.worker_id}] job {job_id} iniciado (prefix={downloads_prefix})")
heartbeat_stop = threading.Event()
heartbeat_thread = threading.Thread(
target=self._heartbeat_while_job_runs,
args=(heartbeat_stop,),
name=f"heartbeat-{self.worker_id}",
daemon=True,
)
heartbeat_thread.start()
try: try:
self.api.download_excel(job_id, excel_path) self.api.download_excel(job_id, excel_path)
@@ -265,3 +307,6 @@ class WorkerAgent:
) )
except Exception: except Exception:
pass pass
finally:
heartbeat_stop.set()
heartbeat_thread.join(timeout=CONTROL_REQUEST_TIMEOUT_SECONDS + 1.0)
+72
View File
@@ -0,0 +1,72 @@
"""Construye un ZIP reproducible y limpio del Bot de Descarga."""
from __future__ import annotations
import argparse
import hashlib
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT = ROOT.parent / "bot_descarga.zip"
TOP_LEVEL_FILES = (
".env.example",
".gitignore",
"README.md",
"requirements.txt",
"run_gui.py",
"main.py",
)
EXCLUDED_PARTS = {"__pycache__", ".pytest_cache", ".venv", "data", "logs", "out"}
EXCLUDED_SUFFIXES = {".pyc", ".pyo", ".log"}
def package_files(root: Path) -> list[Path]:
files: list[Path] = []
for name in TOP_LEVEL_FILES:
path = root / name
if path.is_file():
files.append(path)
package_root = root / "bot_descarga"
for path in package_root.rglob("*"):
relative = path.relative_to(root)
if not path.is_file() or any(part in EXCLUDED_PARTS for part in relative.parts):
continue
if path.suffix.lower() in EXCLUDED_SUFFIXES:
continue
files.append(path)
return sorted(files, key=lambda path: path.relative_to(root).as_posix())
def build_package(root: Path = ROOT, output: Path = DEFAULT_OUTPUT) -> Path:
output.parent.mkdir(parents=True, exist_ok=True)
with ZipFile(output, "w", compression=ZIP_DEFLATED, compresslevel=9) as archive:
for source in package_files(root):
relative = source.relative_to(root).as_posix()
info = ZipInfo(f"bot_descarga/{relative}", date_time=(2020, 1, 1, 0, 0, 0))
info.compress_type = ZIP_DEFLATED
info.external_attr = 0o644 << 16
archive.writestr(info, source.read_bytes())
return output
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
args = parser.parse_args()
target = build_package(output=args.output.resolve())
print(f"Paquete: {target}")
print(f"SHA256: {sha256(target)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+66
View File
@@ -0,0 +1,66 @@
"""Construye y publica el Bot de Descarga en el prefijo latest de MinIO."""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
import boto3
from botocore.config import Config
from package_bot import DEFAULT_OUTPUT, build_package, sha256
DEFAULT_ENDPOINT = "https://s3.builker.xyz"
DEFAULT_BUCKET = "apps-versions"
DEFAULT_KEY = "BotDescargaPYSCuentasMedicas/latest/bot_descarga.zip"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument("--build-only", action="store_true")
args = parser.parse_args()
artifact = build_package(output=args.output.resolve())
print(f"Paquete validado: {artifact} ({sha256(artifact)})")
if args.build_only:
return 0
access = os.getenv("MINIO_ACCESS_KEY") or os.getenv("AWS_ACCESS_KEY_ID")
secret = os.getenv("MINIO_SECRET_KEY") or os.getenv("AWS_SECRET_ACCESS_KEY")
if not access or not secret:
print("Faltan MINIO_ACCESS_KEY/MINIO_SECRET_KEY para publicar.", file=sys.stderr)
return 2
endpoint = os.getenv("MINIO_ENDPOINT", DEFAULT_ENDPOINT).rstrip("/")
bucket = os.getenv("MINIO_APPS_BUCKET", DEFAULT_BUCKET)
key = os.getenv("DOWNLOAD_PACKAGE_KEY", DEFAULT_KEY).strip("/")
client = boto3.client(
"s3",
endpoint_url=endpoint,
aws_access_key_id=access,
aws_secret_access_key=secret,
region_name=os.getenv("MINIO_REGION", "us-east-1"),
config=Config(s3={"addressing_style": "path"}),
)
client.upload_file(
str(artifact),
bucket,
key,
ExtraArgs={"ContentType": "application/zip"},
)
remote = client.head_object(Bucket=bucket, Key=key)
remote_size = int(remote.get("ContentLength") or 0)
if remote_size != artifact.stat().st_size:
print(
f"La verificación remota falló: local={artifact.stat().st_size}, remoto={remote_size}.",
file=sys.stderr,
)
return 3
print(f"Publicado: {endpoint}/{bucket}/{key}")
print(f"Verificado: {remote_size} bytes")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -50,7 +50,7 @@ def test_browser_start_failure_sets_fatal_error(monkeypatch):
svc = DownloadService( svc = DownloadService(
ruta_excel="dummy.xlsx", ruta_excel="dummy.xlsx",
carpeta_descarga=".", carpeta_descarga=".",
settings=Settings(url_sistema="http://serviciosimaging:8180/"), settings=Settings(url_sistema="https://serviciosimaging.saludtotal.loc/"),
event_callback=lambda e: events.append(e), event_callback=lambda e: events.append(e),
) )
summary = svc.run() summary = svc.run()
@@ -0,0 +1,123 @@
import threading
import time
from pathlib import Path
import bot_descarga.worker.agent as agent_module
from bot_descarga.models.resultado import RunSummary
from bot_descarga.worker.agent import WorkerAgent
class FakeApi:
def __init__(self, failures: int = 0):
self.calls = 0
self.failures = failures
def heartbeat(self, worker_id: str) -> dict:
self.calls += 1
if self.calls <= self.failures:
raise RuntimeError("gateway temporalmente no disponible")
return {"worker_id": worker_id}
def make_agent(api: FakeApi) -> WorkerAgent:
agent = WorkerAgent.__new__(WorkerAgent)
agent.api = api
agent.worker_id = "zero0"
agent.heartbeat_seconds = 0.01
agent._log = lambda _message: None
return agent
def run_briefly(agent: WorkerAgent) -> threading.Thread:
stop_event = threading.Event()
thread = threading.Thread(
target=agent._heartbeat_while_job_runs,
args=(stop_event,),
daemon=True,
)
thread.start()
deadline = time.monotonic() + 1.0
while agent.api.calls < 3 and time.monotonic() < deadline:
time.sleep(0.01)
stop_event.set()
thread.join(timeout=1.0)
return thread
def test_active_job_sends_repeated_heartbeats() -> None:
api = FakeApi()
thread = run_briefly(make_agent(api))
assert api.calls >= 3
assert not thread.is_alive()
def test_transient_heartbeat_failure_does_not_stop_loop() -> None:
api = FakeApi(failures=1)
thread = run_briefly(make_agent(api))
assert api.calls >= 3
assert not thread.is_alive()
def test_successful_job_stays_completed_and_stops_heartbeat(monkeypatch, tmp_path: Path) -> None:
class JobApi(FakeApi):
def __init__(self):
super().__init__()
self.patches = []
def download_excel(self, _job_id: str, dest: Path) -> Path:
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(b"xlsx")
return dest
def cancel_flag(self, _job_id: str) -> bool:
return False
def post_event(self, _job_id: str, _event: dict) -> None:
return None
def patch_job(self, job_id: str, **body) -> dict:
self.patches.append((job_id, body))
return body
class FakeMinio:
def write_manifest(self, *_args, **_kwargs) -> None:
return None
class FakeDownloadService:
def __init__(self, **_kwargs):
pass
def run(self) -> RunSummary:
return RunSummary(total=1, exitosos=1)
api = JobApi()
agent = make_agent(api)
agent.staging_root = tmp_path
agent.minio = FakeMinio()
agent._uploaded_keys = []
monkeypatch.setattr(agent_module, "DownloadService", FakeDownloadService)
agent._run_job({"id": "job-1", "excel_key": "input.xlsx"})
calls_after_job = api.calls
time.sleep(0.03)
assert api.patches == [
(
"job-1",
{
"status": "completed",
"result": {
"total": 1,
"exitosos": 1,
"fallidos": 0,
"cancelled": False,
"downloads_prefix": "downloads/job-1/",
"uploaded_count": 0,
},
"error": None,
},
)
]
assert api.calls == calls_after_job
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,18 @@
# ADR 0001: Mantener el heartbeat durante trabajos de descarga
- Estado: Aceptada
- Fecha: 2026-09-04
## Contexto
El bot de descarga ejecuta el procesamiento del Excel de forma bloqueante. Durante ese tiempo el ciclo principal no enviaba heartbeats y el servidor declaraba al worker desconectado después de 45 segundos, reclamando un trabajo que todavía seguía activo.
## Decisión
El worker enviará un heartbeat cada 10 segundos en un hilo auxiliar durante toda la ejecución del trabajo. Las llamadas de heartbeat, eventos y consulta de cancelación tendrán un timeout de 5 segundos y sus fallos temporales no detendrán la descarga local. El servidor esperará 180 segundos antes de reclamar un trabajo por desconexión.
## Consecuencias
- Los trabajos largos no serán reclamados solamente porque el ciclo principal esté ocupado.
- Una interrupción breve del gateway no congelará el navegador durante un minuto por cada llamada de control.
- Si un worker realmente se apaga, la recuperación automática puede tardar hasta tres minutos.
@@ -0,0 +1,147 @@
{"kind":"workbook","id":"wb/vpj3q6","sheets":4,"tables":4}
{"kind":"sheet","id":"ws/85f3jb","name":"Resumen","index":0,"range":"A1:F25","address":"A1:F25","tables":1,"formulas":3}
{"kind":"table","sheet":"Resumen","address":"A1:F25","rows":25,"cols":6,"values":[["Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana"],["Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana"],["Consulta independiente con is_visible = false","Consulta independiente con is_visible = false","Consulta independiente con is_visible = false","Consulta independiente con is_visible = false","Consulta independiente con is_visible = false","Consulta independiente con is_visible = false"],[null,null,null,null,null,null],["Apartamentos",null,"Páginas consultadas",null,"Fotos",null],[0,null,1,null,0,null],[null,null,null,null,null,null],[null,null,null,null,null,null],["Control de extracción","Control de extracción","Control de extracción","Control de extracción","Control de extracción","Control de extracción"],["Fecha y hora (UTC)","2026-08-30T19:41:16.075Z",null,null,null,null],["Origen","https://srv99857.rentpana.com/rest/v1/apartments",null,null,null,null],["Filtro","is_visible = false",null,null,null,null],["Orden estable","id ascendente",null,null,null,null],["Tamaño de página",50,null,null,null,null],["Total informado por API",0,null,null,null,null],["IDs únicos validados",0,null,null,null,null],["Duplicados detectados",0,null,null,null,null],["Columnas de origen",34,null,null,null,null],["Resultado","La API devolvió 0 registros para este filtro con la sesión suministrada.",null,null,null,null],["Credenciales incluidas","No",null,null,null,null],[null,null,null,null,null,null],["Contenido del archivo","Contenido del archivo","Contenido del archivo","Contenido del archivo","Contenido del archivo","Contenido del archivo"],["Apartamentos","Estructura de 34 campos, lista para los registros devueltos por la consulta.",null,null,null,null],["Fotos","Hoja normalizada para enlaces de fotos; actualmente sin filas.",null,null,null,null],["Paginacion","Consulta, límite, rango, cantidad y estado HTTP documentados.",null,null,null,null]]}
{"kind":"region","sheet":"Resumen","address":"A9:F20","rows":12,"cols":6,"nonEmpty":28,"blank":44,"text":28,"maxTextLength":72,"preview":[["Control de extracción","Control de extracción","Control de extracción","Control de extracción","Control de extracción","Control de extracción"],["Fecha y hora (UTC)","46264.82032494213",null,null,null,null],["Origen","https://srv99857.rentpana.com/rest/v1/apartments",null,null,null,null],["Filtro","is_visible = false",null,null,null,null],["Orden estable","id ascendente",null,null,null,null]],"previewAddress":"A9:F13","previewRows":5,"previewCols":6}
{"kind":"region","sheet":"Resumen","address":"A1:F3","rows":3,"cols":6,"nonEmpty":18,"text":18,"maxTextLength":45,"preview":[["Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana"],["Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana","Apartamentos no visibles · RentPana"],["Consulta independiente con is_visible = false","Consulta independiente con is_visible = false","Consulta independiente con is_visible = false","Consulta independiente con is_visible = false","Consulta independiente con is_visible = false","Consulta independiente con is_visible = false"]],"previewAddress":"A1:F3","previewRows":3,"previewCols":6}
{"kind":"region","sheet":"Resumen","address":"A22:F25","rows":4,"cols":6,"nonEmpty":12,"blank":12,"text":12,"maxTextLength":76,"preview":[["Contenido del archivo","Contenido del archivo","Contenido del archivo","Contenido del archivo","Contenido del archivo","Contenido del archivo"],["Apartamentos","Estructura de 34 campos, lista para los registros devueltos por la consulta.",null,null,null,null],["Fotos","Hoja normalizada para enlaces de fotos; actualmente sin filas.",null,null,null,null],["Paginacion","Consulta, límite, rango, cantidad y estado HTTP documentados.",null,null,null,null]],"previewAddress":"A22:F25","previewRows":4,"previewCols":6}
{"kind":"region","sheet":"Resumen","address":"A5:F6","rows":2,"cols":6,"nonEmpty":6,"blank":6,"text":6,"maxTextLength":19,"preview":[["Apartamentos",null,"Páginas consultadas",null,"Fotos",null],["0",null,"1",null,"0",null]],"previewAddress":"A5:F6","previewRows":2,"previewCols":6}
{"kind":"formula","sheet":"Resumen","address":"A6","formula":"=COUNTA('Apartamentos'!$A$2:$A$2)"}
{"kind":"formula","sheet":"Resumen","address":"C6","formula":"=COUNTA('Paginacion'!$A$2:$A$2)"}
{"kind":"formula","sheet":"Resumen","address":"E6","formula":"=COUNTA('Fotos'!$A$2:$A$2)"}
{"kind":"computedStyle","sheet":"Resumen","for":"A1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"B1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"C1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"D1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"E1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"F1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"A2","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"B2","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"C2","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"D2","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"E2","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"F2","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":51}}
{"kind":"computedStyle","sheet":"Resumen","for":"A3","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"D9EDF2"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":52}}
{"kind":"computedStyle","sheet":"Resumen","for":"B3","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"D9EDF2"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":52}}
{"kind":"computedStyle","sheet":"Resumen","for":"C3","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"D9EDF2"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":52}}
{"kind":"computedStyle","sheet":"Resumen","for":"D3","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"D9EDF2"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":52}}
{"kind":"computedStyle","sheet":"Resumen","for":"E3","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"D9EDF2"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":52}}
{"kind":"computedStyle","sheet":"Resumen","for":"F3","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"D9EDF2"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":52}}
{"kind":"computedStyle","sheet":"Resumen","for":"A5","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":54}}
{"kind":"computedStyle","sheet":"Resumen","for":"C5","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":55}}
{"kind":"computedStyle","sheet":"Resumen","for":"E5","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":55}}
{"kind":"computedStyle","sheet":"Resumen","for":"A6","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":57}}
{"kind":"computedStyle","sheet":"Resumen","for":"C6","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":58}}
{"kind":"computedStyle","sheet":"Resumen","for":"E6","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":20,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":58}}
{"kind":"computedStyle","sheet":"Resumen","for":"A9","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"B9","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"C9","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"D9","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"E9","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"F9","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"A10","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":64}}
{"kind":"computedStyle","sheet":"Resumen","for":"B10","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"numberFormat":"yyyy-mm-dd hh:mm:ss","wrapText":true,"horizontalAlignment":"left","styleId":65}}
{"kind":"computedStyle","sheet":"Resumen","for":"A11","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B11","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A12","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B12","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A13","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B13","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A14","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B14","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A15","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B15","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A16","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B16","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A17","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B17","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A18","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B18","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A19","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":68}}
{"kind":"computedStyle","sheet":"Resumen","for":"B19","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"left","styleId":69}}
{"kind":"computedStyle","sheet":"Resumen","for":"A20","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"60707D"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":71}}
{"kind":"computedStyle","sheet":"Resumen","for":"B20","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":true,"horizontalAlignment":"left","styleId":72}}
{"kind":"computedStyle","sheet":"Resumen","for":"A22","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"B22","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"C22","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"D22","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"E22","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"F22","style":{"fill":{"type":1,"color":{"type":1,"value":"0F766E"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":63}}
{"kind":"computedStyle","sheet":"Resumen","for":"A23","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":74}}
{"kind":"computedStyle","sheet":"Resumen","for":"B23","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":true,"horizontalAlignment":"general","styleId":75}}
{"kind":"computedStyle","sheet":"Resumen","for":"A24","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":77}}
{"kind":"computedStyle","sheet":"Resumen","for":"B24","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":true,"horizontalAlignment":"general","styleId":78}}
{"kind":"computedStyle","sheet":"Resumen","for":"A25","style":{"fill":{"type":1,"color":{"type":1,"value":"F4F7F9"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":80}}
{"kind":"computedStyle","sheet":"Resumen","for":"B25","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Aptos","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":true,"horizontalAlignment":"general","styleId":81}}
{"kind":"sheet","id":"ws/ui1vde","name":"Apartamentos","index":1,"range":"A1:AH1","address":"A1:AH1","tables":1}
{"kind":"table","sheet":"Apartamentos","address":"A1:AH1","rows":1,"cols":34,"values":[["id","user_id","user_name","user_phone","address","description","bedrooms","bathrooms","area_m2","monthly_rent","available_rooms","building_amenities","apartment_features","included_services","photo_urls","created_at","updated_at","show_phone","show_profile","is_visible","priority","listing_type","city","area","short_id","smart_rental_enabled","parent_apartment_id","tenant_user_id","tenant_name","tenant_phone","tenant_gender","tenant_age","is_paid","seo_slug"]]}
{"kind":"region","sheet":"Apartamentos","address":"A1:AH1","rows":1,"cols":34,"nonEmpty":34,"text":34,"maxTextLength":20,"preview":[["id","user_id","user_name","user_phone","address","description"]],"previewAddress":"A1:F1","previewRows":1,"previewCols":6}
{"kind":"computedStyle","sheet":"Apartamentos","for":"A1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"B1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"C1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"D1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"E1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"F1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"G1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"H1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"I1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"J1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"K1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"L1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"M1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"N1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"O1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"P1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"Q1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"R1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"S1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"T1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"U1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"V1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"W1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"X1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"Y1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"Z1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"AA1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"AB1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"AC1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"AD1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"AE1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"AF1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"AG1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Apartamentos","for":"AH1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"sheet","id":"ws/x3rkxn","name":"Fotos","index":2,"range":"A1:G1","address":"A1:G1","tables":1}
{"kind":"table","sheet":"Fotos","address":"A1:G1","rows":1,"cols":7,"values":[["apartment_id","short_id","city","area","address","photo_index","photo_url"]]}
{"kind":"region","sheet":"Fotos","address":"A1:G1","rows":1,"cols":7,"nonEmpty":7,"text":7,"maxTextLength":12,"preview":[["apartment_id","short_id","city","area","address","photo_index"]],"previewAddress":"A1:F1","previewRows":1,"previewCols":6}
{"kind":"computedStyle","sheet":"Fotos","for":"A1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Fotos","for":"B1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Fotos","for":"C1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Fotos","for":"D1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Fotos","for":"E1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Fotos","for":"F1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"computedStyle","sheet":"Fotos","for":"G1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"general","styleId":2}}
{"kind":"sheet","id":"ws/ye6w3r","name":"Paginacion","index":3,"range":"A1:H3","address":"A1:H3","tables":1,"formulas":1}
{"kind":"table","sheet":"Paginacion","address":"A1:H3","rows":3,"cols":8,"values":[["pagina","offset","limite","content_range","filas","estado_http","primer_id","ultimo_id"],[1,0,50,"*/0",0,"200 OK",null,null],["Total descargado","Total descargado","Total descargado","Total descargado",0,"Validado contra Content-Range: */0","Validado contra Content-Range: */0","Validado contra Content-Range: */0"]]}
{"kind":"region","sheet":"Paginacion","address":"A1:H3","rows":3,"cols":8,"nonEmpty":22,"blank":2,"text":22,"maxTextLength":34,"preview":[["pagina","offset","limite","content_range","filas","estado_http"],["1","0","50","*/0","0","200 OK"],["Total descargado","Total descargado","Total descargado","Total descargado","0","Validado contra Content-Range: */0"]],"previewAddress":"A1:F3","previewRows":3,"previewCols":6}
{"kind":"formula","sheet":"Paginacion","address":"E3","formula":"=SUM(E2:E2)"}
{"kind":"computedStyle","sheet":"Paginacion","for":"A1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":7}}
{"kind":"computedStyle","sheet":"Paginacion","for":"B1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":7}}
{"kind":"computedStyle","sheet":"Paginacion","for":"C1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":7}}
{"kind":"computedStyle","sheet":"Paginacion","for":"D1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":7}}
{"kind":"computedStyle","sheet":"Paginacion","for":"E1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":7}}
{"kind":"computedStyle","sheet":"Paginacion","for":"F1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":7}}
{"kind":"computedStyle","sheet":"Paginacion","for":"G1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":7}}
{"kind":"computedStyle","sheet":"Paginacion","for":"H1","style":{"fill":{"type":1,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":7}}
{"kind":"computedStyle","sheet":"Paginacion","for":"A2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":10}}
{"kind":"computedStyle","sheet":"Paginacion","for":"B2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":10}}
{"kind":"computedStyle","sheet":"Paginacion","for":"C2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":10}}
{"kind":"computedStyle","sheet":"Paginacion","for":"D2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":10}}
{"kind":"computedStyle","sheet":"Paginacion","for":"E2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":10}}
{"kind":"computedStyle","sheet":"Paginacion","for":"F2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"1F2937"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":10}}
{"kind":"computedStyle","sheet":"Paginacion","for":"A3","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":11}}
{"kind":"computedStyle","sheet":"Paginacion","for":"B3","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":11}}
{"kind":"computedStyle","sheet":"Paginacion","for":"C3","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":11}}
{"kind":"computedStyle","sheet":"Paginacion","for":"D3","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":11}}
{"kind":"computedStyle","sheet":"Paginacion","for":"E3","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":11}}
{"kind":"computedStyle","sheet":"Paginacion","for":"F3","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":11}}
{"kind":"computedStyle","sheet":"Paginacion","for":"G3","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":9}}
{"kind":"computedStyle","sheet":"Paginacion","for":"H3","style":{"fill":{"type":1,"color":{"type":1,"value":"DFF4F1"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"123047"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"bottom":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"left":{"style":"thin","color":{"type":1,"value":"D8E1E7"}},"right":{"style":"thin","color":{"type":1,"value":"D8E1E7"}}},"wrapText":false,"horizontalAlignment":"general","styleId":9}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+3 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.11-slim FROM python:3.11-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
@@ -6,7 +6,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
&& apt-get update \
&& apt-get install -y --no-install-recommends curl \ && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+3 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.12-slim FROM python:3.12-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
@@ -6,7 +6,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
&& apt-get update \
&& apt-get install -y --no-install-recommends curl \ && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@@ -26,7 +26,9 @@ from infrastructure.database import (
) )
from infrastructure.storage_minio import downloads_prefix from infrastructure.storage_minio import downloads_prefix
HEARTBEAT_OFFLINE_SECONDS = 45 # El worker procesa el navegador de forma bloqueante. Tres minutos toleran una
# caída breve del gateway sin reclamar el mismo job mientras sigue descargando.
HEARTBEAT_OFFLINE_SECONDS = 180
# Un agent recién conectado no recibe jobs reclamados de otros workers. # Un agent recién conectado no recibe jobs reclamados de otros workers.
_FRESH_WORKER_GRACE_SECONDS = 45 _FRESH_WORKER_GRACE_SECONDS = 45
+3 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.12-slim FROM python:3.12-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
@@ -6,7 +6,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
&& apt-get update \
&& apt-get install -y --no-install-recommends curl \ && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+3 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.11-slim FROM python:3.11-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
@@ -6,7 +6,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
&& apt-get update \
&& apt-get install -y --no-install-recommends libgl1 libglib2.0-0 \ && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.12-slim FROM python:3.12-slim-bookworm
WORKDIR /app WORKDIR /app
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ipsextractor-desktop", "name": "ipsextractor-desktop",
"version": "1.3.1", "version": "1.3.2",
"private": true, "private": true,
"description": "Aplicación de escritorio Flujo IPS para automatizar procesos de cuentas médicas.", "description": "Aplicación de escritorio Flujo IPS para automatizar procesos de cuentas médicas.",
"main": "electron/main.cjs", "main": "electron/main.cjs",
+3 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.11-slim FROM python:3.11-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
@@ -6,7 +6,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
&& apt-get update \
&& apt-get install -y --no-install-recommends poppler-utils libgl1 libglib2.0-0 \ && apt-get install -y --no-install-recommends poppler-utils libgl1 libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11-slim FROM python:3.11-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
@@ -29,7 +29,22 @@ ENV GOMAXPROCS=2
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download # La red hacia proxy.golang.org puede ser intermitente en Docker Desktop.
# Reintentar conserva la verificación de go.sum; si el proxy oficial sigue
# caído, la última tentativa descarga directamente desde el repositorio origen.
RUN --mount=type=cache,target=/go/pkg/mod \
set -eu; \
attempt=1; \
while [ "$attempt" -le 3 ]; do \
if GOPROXY=https://proxy.golang.org go mod download; then \
exit 0; \
fi; \
echo "go mod download falló (intento ${attempt}/3); reintentando..."; \
sleep $((attempt * 5)); \
attempt=$((attempt + 1)); \
done; \
echo "proxy.golang.org no respondió; intentando descarga directa..."; \
GOPROXY=direct go mod download
COPY . . COPY . .