diff --git a/index.js b/index.js index 90ac928..e9e33c6 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,3 @@ -// index.js 'use strict'; const express = require('express'); @@ -6,6 +5,8 @@ const cors = require('cors'); const { chromium } = require('playwright'); const admin = require('firebase-admin'); +// -------------------- Firebase -------------------- + function mustEnv(name) { const v = process.env[name]; if (!v) throw new Error(`Missing env: ${name}`); @@ -15,62 +16,104 @@ function mustEnv(name) { function initFirebase() { if (admin.apps?.length) return admin.firestore(); - // Requiere estas 3 env (como ya usas en tus robots) - if (!process.env.FIREBASE_PRIVATE_KEY) throw new Error('Missing env: FIREBASE_PRIVATE_KEY'); + if (!process.env.FIREBASE_PRIVATE_KEY) { + throw new Error('Missing env: FIREBASE_PRIVATE_KEY'); + } + admin.initializeApp({ credential: admin.credential.cert({ projectId: mustEnv('FIREBASE_PROJECT_ID'), clientEmail: mustEnv('FIREBASE_CLIENT_EMAIL'), - privateKey: mustEnv('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'), + privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), }), }); + return admin.firestore(); } -const db = initFirebase(); +async function getHomeServeCreds(db) { + // Tu caso real: providerCredentials/homeserve con { user, pass } + const docPath = process.env.HS_CRED_DOC_PATH || 'providerCredentials/homeserve'; + + let fsUser = ''; + let fsPass = ''; + + try { + const snap = await db.doc(docPath).get(); + if (snap.exists) { + const d = snap.data() || {}; + fsUser = String(d.user || '').trim(); + fsPass = String(d.pass || '').trim(); + } + } catch (_) {} + + const envUser = String(process.env.HOMESERVE_USER || '').trim(); + const envPass = String(process.env.HOMESERVE_PASS || '').trim(); + + const user = fsUser || envUser; + const pass = fsPass || envPass; + + if (!user || !pass) { + throw new Error( + `HomeServe creds missing. Put {user,pass} in Firestore doc "${docPath}" or set env HOMESERVE_USER/HOMESERVE_PASS` + ); + } + + return { user, pass, source: fsUser ? `firestore:${docPath}` : 'env' }; +} + +// -------------------- Config -------------------- -// ===== Config ===== const CONFIG = { - SERVICE_NAME: 'estados-hs', - + // Flujo antiguo: HOMESERVE_BASE_URL: process.env.HOMESERVE_BASE_URL || 'https://gestor.homeserve.es/', - // 🔥 Preferimos este doc (tu captura) - HS_CRED_DOC_PATH: process.env.HS_CRED_DOC_PATH || 'providerCredentials/homeserve', - HS_CRED_DOC_FALLBACK: process.env.HS_CRED_DOC_FALLBACK || 'secrets/homeserve', - - // Auth opcional al endpoint (si lo quieres) - REQUIRE_AUTH: String(process.env.REQUIRE_AUTH || '0') === '1', - AUTH_TOKEN: process.env.AUTH_TOKEN || '', - - // Playwright - HEADLESS: String(process.env.HEADLESS || 'true') !== 'false', - - // Selectores (ajústalos si difieren) + // Selectores (igual que el robot viejo, pero con extras) SEL: { - user: process.env.SEL_USER || 'input[type="text"]', - pass: process.env.SEL_PASS || 'input[type="password"]', - submit: process.env.SEL_SUBMIT || 'button[type="submit"]', + user: process.env.SEL_USER || 'input[type="text"], input[name="user"], input[name="username"]', + pass: process.env.SEL_PASS || 'input[type="password"], input[name="pass"], input[name="password"]', + submit: process.env.SEL_SUBMIT || 'button[type="submit"], input[type="submit"], button:has-text("Entrar")', - // búsqueda de parte/servicio searchBox: process.env.SEL_SEARCH_BOX || 'input[placeholder*="Buscar"], input[type="search"]', searchBtn: process.env.SEL_SEARCH_BTN || 'button:has-text("Buscar"), button:has-text("Search")', - - // entrar al detalle del parte openRow: process.env.SEL_OPEN_ROW || 'table tbody tr:first-child', - // cambio de estado - statusDropdown: process.env.SEL_STATUS_DROPDOWN || 'select[name*="estado"], select[id*="estado"], select:has(option)', + // Cambio estado + statusDropdown: + process.env.SEL_STATUS_DROPDOWN || + 'select[name*="estado" i], select[id*="estado" i], select:has(option)', - // (opcionales) campos de fecha / observación si existen en HomeServe - dateInput: process.env.SEL_DATE_INPUT || 'input[name*="fecha"], input[id*="fecha"], input[placeholder*="dd"], input[placeholder*="DD"]', - noteTextarea: process.env.SEL_NOTE_TEXTAREA || 'textarea[name*="nota"], textarea[id*="nota"], textarea', - saveBtn: process.env.SEL_SAVE_BTN || 'button:has-text("Guardar"), button:has-text("Save"), button:has-text("Actualizar")', + // Observación/nota + noteTextarea: + process.env.SEL_NOTE_TEXTAREA || + 'textarea[name*="nota" i], textarea[id*="nota" i], textarea', + + // Fecha (si existe) + dateInput: + process.env.SEL_DATE_INPUT || + 'input[name*="fec" i], input[id*="fec" i], input[placeholder*="dd" i], input[placeholder*="fecha" i]', + + // Checkbox “ya informado al cliente” + informedCheckbox: + process.env.SEL_INFORMED_CHECKBOX || + 'input[type="checkbox"][name*="inform" i], input[type="checkbox"][id*="inform" i]', + + // Botón guardar + saveBtn: + process.env.SEL_SAVE_BTN || + 'button:has-text("Guardar"), button:has-text("Save"), button:has-text("Actualizar"), input[type="submit"]', }, + + HEADLESS: String(process.env.HEADLESS || '1') !== '0', + NAV_TIMEOUT_MS: parseInt(process.env.NAV_TIMEOUT_MS || '120000', 10), + + // Auth opcional del endpoint + REQUIRE_AUTH: String(process.env.REQUIRE_AUTH || '0') === '1', + API_KEY: process.env.API_KEY || '', }; -// ===== Estados (los del switch de tu app) ===== -const STATUS = [ +// Estados por código (los de tu app) +const STATUS_CODES = [ { code: '303', title: 'En espera de Cliente por aceptación Presupuesto' }, { code: '307', title: 'En espera de Profesional por fecha de inicio de trabajos' }, { code: '313', title: 'En espera de Profesional por secado de cala, pintura o parquet' }, @@ -87,57 +130,20 @@ const STATUS = [ { code: '352', title: 'En espera de Perjudicado por indicaciones' }, ]; -const CODE_TO_LABEL = Object.fromEntries(STATUS.map(s => [s.code, s.title])); +const CODE_TO_TITLE = new Map(STATUS_CODES.map(x => [x.code, x.title])); -// ===== Utils ===== -const sleep = (ms) => new Promise(r => setTimeout(r, ms)); +// -------------------- Helpers -------------------- + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const nowISO = () => new Date().toISOString(); +const trim = (v) => String(v ?? '').trim(); -function parseDocPath(path) { - // "collection/doc/subcollection/doc" -> solo soportamos colección/doc o colección/doc/... (FireStore permite) - const parts = String(path || '').split('/').filter(Boolean); - if (parts.length < 2 || parts.length % 2 !== 0) { - throw new Error(`Invalid Firestore doc path: "${path}" (must be collection/doc[/collection/doc...])`); - } - let ref = db.collection(parts[0]).doc(parts[1]); - for (let i = 2; i < parts.length; i += 2) { - ref = ref.collection(parts[i]).doc(parts[i + 1]); - } - return ref; -} - -async function getHomeServeCreds() { - // 1) Firestore doc principal (providerCredentials/homeserve) - const tryDoc = async (path) => { - try { - const ref = parseDocPath(path); - const snap = await ref.get(); - if (!snap.exists) return null; - const d = snap.data() || {}; - const user = (d.user || d.username || '').toString().trim(); - const pass = (d.pass || d.password || '').toString().trim(); - if (!user || !pass) return null; - return { user, pass, source: `firestore:${path}` }; - } catch (_) { - return null; - } - }; - - const a = await tryDoc(CONFIG.HS_CRED_DOC_PATH); - if (a) return a; - - const b = await tryDoc(CONFIG.HS_CRED_DOC_FALLBACK); - if (b) return b; - - // 2) Env fallback - const envUser = (process.env.HOMESERVE_USER || '').trim(); - const envPass = (process.env.HOMESERVE_PASS || '').trim(); - if (envUser && envPass) return { user: envUser, pass: envPass, source: 'env:HOMESERVE_USER/HOMESERVE_PASS' }; - - throw new Error( - `HomeServe creds missing. Set them in Firestore doc "${CONFIG.HS_CRED_DOC_PATH}" (user/pass) ` + - `or "${CONFIG.HS_CRED_DOC_FALLBACK}" (user/pass) or env HOMESERVE_USER/HOMESERVE_PASS` - ); +function parseBool(v, def = true) { + if (v === undefined || v === null || v === '') return def; + const s = String(v).toLowerCase().trim(); + if (['1', 'true', 'yes', 'si', 'sí'].includes(s)) return true; + if (['0', 'false', 'no'].includes(s)) return false; + return def; } async function withBrowser(fn) { @@ -145,7 +151,7 @@ async function withBrowser(fn) { headless: CONFIG.HEADLESS, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); - const context = await browser.newContext(); + const context = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const page = await context.newPage(); try { return await fn(page); @@ -154,8 +160,10 @@ async function withBrowser(fn) { } } +// -------------------- Playwright flow (como el robot antiguo) -------------------- + async function login(page, creds) { - await page.goto(CONFIG.HOMESERVE_BASE_URL, { waitUntil: 'domcontentloaded', timeout: 120000 }); + await page.goto(CONFIG.HOMESERVE_BASE_URL, { waitUntil: 'domcontentloaded', timeout: CONFIG.NAV_TIMEOUT_MS }); await page.waitForSelector(CONFIG.SEL.user, { timeout: 60000 }); await page.fill(CONFIG.SEL.user, creds.user); @@ -167,122 +175,277 @@ async function login(page, creds) { if (btn) await btn.click(); else await page.keyboard.press('Enter'); - await page.waitForLoadState('networkidle', { timeout: 120000 }); + // networkidle a veces es traicionero; domcontentloaded + pause suele ir mejor + await page.waitForLoadState('domcontentloaded', { timeout: CONFIG.NAV_TIMEOUT_MS }); + await sleep(1200); } -async function openParte(page, serviceNumber) { +async function openParte(page, parteId) { const hasSearch = await page.$(CONFIG.SEL.searchBox); if (hasSearch) { - await page.fill(CONFIG.SEL.searchBox, String(serviceNumber)); + await page.fill(CONFIG.SEL.searchBox, String(parteId)); const btn = await page.$(CONFIG.SEL.searchBtn); if (btn) await btn.click(); else await page.keyboard.press('Enter'); - await page.waitForLoadState('networkidle', { timeout: 120000 }); + await page.waitForLoadState('domcontentloaded', { timeout: CONFIG.NAV_TIMEOUT_MS }); await sleep(1200); } await page.waitForSelector(CONFIG.SEL.openRow, { timeout: 60000 }); await page.click(CONFIG.SEL.openRow); - await page.waitForLoadState('networkidle', { timeout: 120000 }); + await page.waitForLoadState('domcontentloaded', { timeout: CONFIG.NAV_TIMEOUT_MS }); await sleep(900); } -async function setEstado(page, code, dateString, observation) { +async function checkInformedBox(page) { + // 1) intento selector directo + const cb = await page.$(CONFIG.SEL.informedCheckbox); + if (cb) { + const isChecked = await cb.isChecked().catch(() => false); + if (!isChecked) await cb.check().catch(() => {}); + return true; + } + + // 2) intento por texto de label (tu frase exacta) + const ok = await page.evaluate(() => { + const text = 'Marque esta casilla, si ya ha informado al Cliente'.toLowerCase(); + const labels = Array.from(document.querySelectorAll('label')); + const hit = labels.find(l => (l.textContent || '').toLowerCase().includes(text)); + if (!hit) return false; + + const forId = hit.getAttribute('for'); + if (forId) { + const input = document.getElementById(forId); + if (input && input.type === 'checkbox') { + input.checked = true; + input.dispatchEvent(new Event('change', { bubbles: true })); + return true; + } + } + + // si no hay "for", buscamos checkbox cercano + let el = hit; + for (let i = 0; i < 4; i++) { + const parent = el.parentElement; + if (!parent) break; + const cb2 = parent.querySelector('input[type="checkbox"]'); + if (cb2) { + cb2.checked = true; + cb2.dispatchEvent(new Event('change', { bubbles: true })); + return true; + } + el = parent; + } + return false; + }); + + return !!ok; +} + +async function setEstadoByCodeOrLabel(page, newStatusValue) { await page.waitForSelector(CONFIG.SEL.statusDropdown, { timeout: 60000 }); - // 1) intenta por value (= código) - let selected = false; + // Primero intentamos por VALUE (si el select usa value=303 etc) try { - await page.selectOption(CONFIG.SEL.statusDropdown, { value: String(code) }); - selected = true; + await page.selectOption(CONFIG.SEL.statusDropdown, { value: String(newStatusValue) }); + return { method: 'value', picked: String(newStatusValue) }; } catch (_) {} - // 2) si no, intenta por label (título del estado) - if (!selected) { - const label = CODE_TO_LABEL[String(code)] || String(code); + // Segundo: intentamos por label con el texto del código (si el option pinta "348 - En espera...") + const title = CODE_TO_TITLE.get(String(newStatusValue)); + const candidates = []; + + if (title) { + candidates.push(`${newStatusValue} · ${title}`); + candidates.push(`${newStatusValue} - ${title}`); + candidates.push(`${newStatusValue} ${title}`); + candidates.push(title); + } else { + candidates.push(String(newStatusValue)); + } + + for (const label of candidates) { try { await page.selectOption(CONFIG.SEL.statusDropdown, { label }); - selected = true; + return { method: 'label', picked: label }; } catch (_) {} } - // 3) si no, intenta encontrar option cuyo text contenga el código o el título - if (!selected) { - const label = CODE_TO_LABEL[String(code)] || ''; - const ok = await page.evaluate(({ sel, code, label }) => { - const s = document.querySelector(sel); - if (!s) return false; - const opts = Array.from(s.querySelectorAll('option')); - const lc = String(code).trim().toLowerCase(); - const ll = String(label).trim().toLowerCase(); - const hit = opts.find(o => { - const t = (o.textContent || '').trim().toLowerCase(); - return t.includes(lc) || (!!ll && t.includes(ll)); - }); - if (!hit) return false; - s.value = hit.value; - s.dispatchEvent(new Event('change', { bubbles: true })); - return true; - }, { sel: CONFIG.SEL.statusDropdown, code: String(code), label }); + // Tercero: buscar en DOM option que contenga el código + const ok = await page.evaluate(({ sel, code }) => { + const s = document.querySelector(sel); + if (!s) return null; + const opts = Array.from(s.querySelectorAll('option')); + const hit = + opts.find(o => String(o.value).trim() === String(code).trim()) || + opts.find(o => (o.textContent || '').includes(String(code))); + if (!hit) return null; + s.value = hit.value; + s.dispatchEvent(new Event('change', { bubbles: true })); + return { value: hit.value, text: (hit.textContent || '').trim() }; + }, { sel: CONFIG.SEL.statusDropdown, code: String(newStatusValue) }); - if (!ok) throw new Error(`No matching status option for code "${code}"`); + if (!ok) throw new Error(`No matching status option for code "${newStatusValue}"`); + return { method: 'dom', picked: ok.value, text: ok.text }; +} + +async function fillDateIfExists(page, dateString) { + if (!trim(dateString)) return false; + const el = await page.$(CONFIG.SEL.dateInput); + if (!el) return false; + try { + await page.fill(CONFIG.SEL.dateInput, String(dateString)); + return true; + } catch (_) { + return false; } +} - // Fecha (si existe el campo) - if (dateString) { - const el = await page.$(CONFIG.SEL.dateInput); - if (el) { - await page.fill(CONFIG.SEL.dateInput, String(dateString)); - } - } - - // Nota / observación (si existe) - if (observation) { - const ta = await page.$(CONFIG.SEL.noteTextarea); - if (ta) { - await page.fill(CONFIG.SEL.noteTextarea, String(observation)); - } - } +async function fillObservationIfExists(page, observation) { + if (!trim(observation)) return false; + const ta = await page.$(CONFIG.SEL.noteTextarea); + if (!ta) return false; + await page.fill(CONFIG.SEL.noteTextarea, String(observation)); + return true; +} +async function clickSave(page) { const save = await page.$(CONFIG.SEL.saveBtn); if (!save) throw new Error('Save button not found'); await save.click(); - - await page.waitForLoadState('networkidle', { timeout: 120000 }); + await page.waitForLoadState('domcontentloaded', { timeout: CONFIG.NAV_TIMEOUT_MS }); await sleep(1200); } -// ===== Express ===== -const app = express(); +async function changeStatusViaGestor(page, job, creds) { + await login(page, creds); + await openParte(page, job.serviceNumber); -app.use(cors()); + const pick = await setEstadoByCodeOrLabel(page, job.newStatusValue); + + const informed = job.informoCliente ? await checkInformedBox(page) : false; + const dateFilled = await fillDateIfExists(page, job.dateString); + const obsFilled = await fillObservationIfExists(page, job.observation); + + await clickSave(page); + + const title = await page.title().catch(() => ''); + const snippet = await page.evaluate(() => (document.body ? document.body.innerText : '')).catch(() => ''); + return { + ok: true, + flow: 'gestor', + picked: pick, + informedChecked: informed, + dateFilled, + observationFilled: obsFilled, + pageTitle: title, + snippet: trim(snippet).slice(0, 600), + }; +} + +// -------------------- API server -------------------- + +const app = express(); +app.use(cors({ origin: true })); app.use(express.json({ limit: '1mb' })); -// Root OK (lo que pediste) -app.get('/', (req, res) => { - res.status(200).send('ok'); -}); +app.get('/', (req, res) => res.status(200).send('ok')); app.get('/health', (req, res) => { + const port = Number(process.env.PORT || process.env.CAPROVER_PORT || 3000); res.json({ ok: true, - service: CONFIG.SERVICE_NAME, + service: 'estados-hs', + port, requireAuth: CONFIG.REQUIRE_AUTH, - hsCredDocPath: CONFIG.HS_CRED_DOC_PATH, - hsCredDocFallback: CONFIG.HS_CRED_DOC_FALLBACK, + hsCredDocPath: process.env.HS_CRED_DOC_PATH || 'providerCredentials/homeserve', + baseUrl: CONFIG.HOMESERVE_BASE_URL, ts: nowISO(), }); }); -// HTML integrado para pruebas (puedes dejarlo así o servir un archivo) -app.get('/test', (req, res) => { - const optionsHtml = STATUS.map(s => { - const sel = s.code === '348' ? ' selected' : ''; - return ``; - }).join('\n'); +app.get('/statuses', (req, res) => { + res.json({ ok: true, statuses: STATUS_CODES }); +}); - res.status(200).send(` +function authMiddleware(req, res, next) { + if (!CONFIG.REQUIRE_AUTH) return next(); + if (!CONFIG.API_KEY) return res.status(500).json({ ok: false, error: { message: 'REQUIRE_AUTH=1 but API_KEY not set' } }); + + const k = trim(req.headers['x-api-key'] || '') || trim(req.query.apiKey || ''); + if (k !== CONFIG.API_KEY) return res.status(401).json({ ok: false, error: { message: 'Unauthorized' } }); + next(); +} + +// HTML de prueba servido desde el propio server (por si te da pereza subir un .html) +app.get('/test', (req, res) => { + res.type('html').send(buildTestHtml()); +}); + +app.post('/api/homeserve/change-status', authMiddleware, async (req, res) => { + const startedAtISO = nowISO(); + + const job = { + serviceNumber: trim(req.body?.serviceNumber), + newStatusValue: trim(req.body?.newStatusValue), + dateString: trim(req.body?.dateString), + observation: trim(req.body?.observation), + informoCliente: parseBool(req.body?.informoCliente, true), // por defecto TRUE + }; + + if (!job.serviceNumber || !job.newStatusValue) { + return res.status(400).json({ + ok: false, + startedAtISO, + finishedAtISO: nowISO(), + error: { message: 'Missing serviceNumber or newStatusValue' }, + }); + } + + let db; + try { + db = initFirebase(); + } catch (e) { + return res.status(500).json({ + ok: false, + startedAtISO, + finishedAtISO: nowISO(), + error: { message: String(e?.message || e), stack: String(e?.stack || '') }, + }); + } + + try { + const creds = await getHomeServeCreds(db); + + const result = await withBrowser(async (page) => { + return await changeStatusViaGestor(page, job, creds); + }); + + return res.json({ + ok: true, + startedAtISO, + finishedAtISO: nowISO(), + request: job, + result, + }); + } catch (err) { + return res.status(500).json({ + ok: false, + startedAtISO, + finishedAtISO: nowISO(), + request: job, + error: { message: String(err?.message || err), stack: String(err?.stack || '') }, + }); + } +}); + +function buildTestHtml() { + const options = STATUS_CODES.map( + (s) => `` + ).join('\n'); + + return ` @@ -290,27 +453,34 @@ app.get('/test', (req, res) => { estados-hs · test
-

estados-hs · prueba

-
Servidor:
+

estados-hs · pruebas (flujo gestor)

+
Consejo: si /health responde JSON, el server está vivo. Si POST devuelve ok:true, ya estás cambiando estado 🔧
-
- - + + + +
+ +
+
+ @@ -318,7 +488,7 @@ app.get('/test', (req, res) => {
@@ -328,10 +498,15 @@ app.get('/test', (req, res) => {
- + + +
+ + +
- +
Listo.
@@ -339,27 +514,27 @@ app.get('/test', (req, res) => { -`); -}); +`; +} -// Endpoint principal: cambio directo -app.post('/api/homeserve/change-status', async (req, res) => { - try { - if (CONFIG.REQUIRE_AUTH) { - const auth = req.headers.authorization || ''; - const token = auth.startsWith('Bearer ') ? auth.slice(7) : ''; - if (!CONFIG.AUTH_TOKEN || token !== CONFIG.AUTH_TOKEN) { - return res.status(401).json({ ok: false, error: { message: 'Unauthorized' } }); - } - } - - const serviceNumber = String(req.body?.serviceNumber || '').trim(); - const newStatusValue = String(req.body?.newStatusValue || '').trim(); // código - const dateString = String(req.body?.dateString || '').trim(); - const observation = String(req.body?.observation || '').trim(); - - if (!serviceNumber) return res.status(400).json({ ok: false, error: { message: 'Missing serviceNumber' } }); - if (!newStatusValue) return res.status(400).json({ ok: false, error: { message: 'Missing newStatusValue' } }); - - const startedAtISO = nowISO(); - const creds = await getHomeServeCreds(); - - console.log(`[${CONFIG.SERVICE_NAME}] change-status service=${serviceNumber} status=${newStatusValue} credsSource=${creds.source}`); - - await withBrowser(async (page) => { - await login(page, creds); - await openParte(page, serviceNumber); - await setEstado(page, newStatusValue, dateString || null, observation || null); - }); - - const finishedAtISO = nowISO(); - - res.json({ - ok: true, - serviceNumber, - newStatusValue, - startedAtISO, - finishedAtISO, - }); - - } catch (err) { - const finishedAtISO = nowISO(); - res.status(500).json({ - ok: false, - finishedAtISO, - error: { - message: String(err?.message || err), - stack: String(err?.stack || ''), - }, - }); - } -}); - -// Helpers function escapeHtml(s) { return String(s || '') .replaceAll('&', '&') @@ -449,18 +570,11 @@ function escapeHtml(s) { .replaceAll("'", '''); } -// ===== Listen (compatible CapRover) ===== -const portA = parseInt(process.env.CAPROVER_PORT || process.env.PORT || '3000', 10); -const portB = 80; // por si CapRover está esperando 80 y no configuraste el "Container HTTP Port" - -console.log(`[${CONFIG.SERVICE_NAME}] HS_CRED_DOC_PATH=${CONFIG.HS_CRED_DOC_PATH}`); -console.log(`[${CONFIG.SERVICE_NAME}] HS_CRED_DOC_FALLBACK=${CONFIG.HS_CRED_DOC_FALLBACK}`); -console.log(`[${CONFIG.SERVICE_NAME}] REQUIRE_AUTH=${CONFIG.REQUIRE_AUTH ? 1 : 0}`); -console.log(`[${CONFIG.SERVICE_NAME}] ENV PORT=${process.env.PORT || '(unset)'} CAPROVER_PORT=${process.env.CAPROVER_PORT || '(unset)'}`); - -app.listen(portA, () => console.log(`[${CONFIG.SERVICE_NAME}] listening on :${portA}`)); - -// Si el puerto principal no es 80, abrimos 80 también para evitar 502 (si CapRover está esperando 80) -if (portA !== portB) { - app.listen(portB, () => console.log(`[${CONFIG.SERVICE_NAME}] listening on :${portB}`)); -} \ No newline at end of file +const PORT = Number(process.env.PORT || process.env.CAPROVER_PORT || 3000); +app.listen(PORT, () => { + console.log(`[estados-hs] listening on :${PORT}`); + console.log(`[estados-hs] HOMESERVE_BASE_URL=${CONFIG.HOMESERVE_BASE_URL}`); + console.log(`[estados-hs] HS_CRED_DOC_PATH=${process.env.HS_CRED_DOC_PATH || 'providerCredentials/homeserve'}`); + console.log(`[estados-hs] REQUIRE_AUTH=${CONFIG.REQUIRE_AUTH ? 1 : 0}`); + console.log(`[estados-hs] HEADLESS=${CONFIG.HEADLESS ? 1 : 0}`); +}); \ No newline at end of file