{"id":15,"date":"2026-04-07T16:49:06","date_gmt":"2026-04-07T14:49:06","guid":{"rendered":"https:\/\/servidorencasa.com\/?page_id=15"},"modified":"2026-04-07T16:49:06","modified_gmt":"2026-04-07T14:49:06","slug":"timbre","status":"publish","type":"page","link":"https:\/\/servidorencasa.com\/index.php\/timbre\/","title":{"rendered":"Timbre"},"content":{"rendered":"    <style>\n      :root{\n        --bg:#0b0b0b;\n        --bg2:#151515;\n        --fg:#f2f2f2;\n        --b2:#2b2b2b;\n        --b3:#444;\n        --btn:#1f6feb;\n        --btnFg:#fff;\n        --ok:#1a7f37;\n        --err:#b3261e;\n        --warn:#e6b800;\n        --info:#6ea8fe;\n      }\n\n      .sec-wrap[data-theme=\"light\"]{\n        --bg:#f6f8fa;\n        --bg2:#ffffff;\n        --fg:#111111;\n        --b2:#d0d7de;\n        --b3:#c8c8c8;\n        --btn:#0969da;\n        --btnFg:#ffffff;\n      }\n\n      .sec-wrap *{box-sizing:border-box}\n\n      .sec-wrap{\n        width:min(760px,94vw);\n        margin:30px auto;\n        background:var(--bg2);\n        border:1px solid var(--b2);\n        border-radius:18px;\n        padding:1.25rem 1.5rem 1.5rem;\n        box-shadow:0 10px 30px rgba(0,0,0,.25);\n        position:relative;\n        color:var(--fg);\n        font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;\n      }\n\n      .sec-wrap h1{\n        margin:.25rem 0 .75rem;\n        font-size:1.55rem;\n      }\n\n      .sec-theme-toggle{\n        position:absolute;\n        right:.9rem;\n        top:.9rem;\n        background:transparent;\n        color:inherit;\n        border:1px solid var(--b2);\n        border-radius:10px;\n        padding:.45rem .6rem;\n        cursor:pointer;\n      }\n\n      .sec-row{\n        display:grid;\n        gap:.8rem;\n        margin:.8rem 0;\n      }\n\n      .sec-wrap label{\n        font-size:.95rem;\n        opacity:.92;\n        display:block;\n        margin-bottom:.35rem;\n      }\n\n      .sec-wrap input,\n      .sec-wrap select{\n        width:100%;\n        padding:.7rem .8rem;\n        border-radius:10px;\n        border:1px solid var(--b2);\n        background:transparent;\n        color:var(--fg);\n      }\n\n      .sec-actions{\n        display:flex;\n        gap:.6rem;\n        flex-wrap:wrap;\n        align-items:center;\n        margin-top:.7rem;\n      }\n\n      .sec-wrap button{\n        padding:.9rem 1rem;\n        border-radius:10px;\n        border:1px solid var(--b3);\n        background:var(--btn);\n        color:var(--btnFg);\n        font-weight:800;\n        cursor:pointer;\n      }\n\n      .sec-wrap button.secondary{\n        background:var(--bg2);\n        color:var(--fg);\n      }\n\n      .sec-wrap button:disabled{\n        opacity:.6;\n        cursor:not-allowed;\n      }\n\n      .sec-muted{\n        opacity:.88;\n        font-size:.95rem;\n      }\n\n      .sec-msg{\n        margin-top:1rem;\n        padding:.85rem 1rem;\n        border-radius:10px;\n        border:1px solid var(--b2);\n      }\n\n      .sec-msg.ok{border-color:var(--ok)}\n      .sec-msg.err{border-color:var(--err)}\n      .sec-msg.warn{border-color:var(--warn)}\n      .sec-msg.info{border-color:var(--info)}\n\n      .sec-hidden{display:none}\n      .sec-screen{display:none}\n      .sec-screen.active{display:block}\n\n      @media (min-width: 700px){\n        .sec-row.two-cols{\n          grid-template-columns:1fr 1fr;\n        }\n      }\n    <\/style>\n\n    <div class=\"sec-wrap\" id=\"sec-timbre-wrap\" data-theme=\"dark\">\n      <button id=\"secThemeToggle\" class=\"sec-theme-toggle\" type=\"button\" aria-label=\"Cambiar tema\">\ud83c\udf19<\/button>\n\n      <h1>\ud83d\udd14 Timbre de Tino<\/h1>\n      <div class=\"sec-muted\">Este timbre solo funciona dentro de la zona permitida (GPS).<\/div>\n\n      <section id=\"secPantalla1\" class=\"sec-screen active\">\n        <h3 style=\"margin:.9rem 0 .4rem\">1) Registro (solo la primera vez)<\/h3>\n\n        <div class=\"sec-row two-cols\">\n          <div>\n            <label for=\"secCodigo\">C\u00f3digo de acceso<\/label>\n            <input id=\"secCodigo\" maxlength=\"50\" placeholder=\"Ej.: Tinolois123\" autocomplete=\"off\">\n          <\/div>\n\n          <div>\n            <label for=\"secPin\">PIN (4 cifras)<\/label>\n            <input id=\"secPin\" inputmode=\"numeric\" maxlength=\"4\" placeholder=\"Ej.: 1234\" autocomplete=\"off\">\n          <\/div>\n        <\/div>\n\n        <div class=\"sec-row two-cols\">\n          <div>\n            <label for=\"secNombre\">Tu nombre<\/label>\n            <input id=\"secNombre\" maxlength=\"100\" placeholder=\"Ej.: Paco\" autocomplete=\"name\">\n          <\/div>\n\n          <div>\n            <label for=\"secRelacion\">\u00bfQui\u00e9n eres?<\/label>\n            <select id=\"secRelacion\">\n              <option value=\"\">Elige una opci\u00f3n\u2026<\/option>\n              <option>Hermano<\/option>\n              <option>Hermana<\/option>\n              <option>Sobrino<\/option>\n              <option>Sobrina<\/option>\n              <option>Amigo<\/option>\n              <option>Amiga<\/option>\n              <option>Vecino<\/option>\n              <option>Vecina<\/option>\n              <option>Familiar<\/option>\n              <option>Reparto<\/option>\n              <option>Otro<\/option>\n              <option>Tio<\/option>\n              <option>Tia<\/option>\n            <\/select>\n          <\/div>\n        <\/div>\n\n        <div class=\"sec-actions\">\n          <button id=\"secBtnRegistro\" type=\"button\">Pulsa aqu\u00ed para confirmar<\/button>\n          <button id=\"secBtnCerrarSesion1\" class=\"secondary\" type=\"button\">Cerrar sesi\u00f3n<\/button>\n        <\/div>\n      <\/section>\n\n      <section id=\"secPantalla2\" class=\"sec-screen\">\n        <h3 style=\"margin:.9rem 0 .4rem\">2) Llamar<\/h3>\n\n        <div class=\"sec-row two-cols\">\n          <div>\n            <label for=\"secNombre2\">Tu nombre<\/label>\n            <input id=\"secNombre2\" autocomplete=\"name\">\n          <\/div>\n\n          <div>\n            <label for=\"secRelacion2\">\u00bfQui\u00e9n eres?<\/label>\n            <select id=\"secRelacion2\">\n              <option>Hermano<\/option>\n              <option>Hermana<\/option>\n              <option>Sobrino<\/option>\n              <option>Sobrina<\/option>\n              <option>Amigo<\/option>\n              <option>Amiga<\/option>\n              <option>Vecino<\/option>\n              <option>Vecina<\/option>\n              <option>Familiar<\/option>\n              <option>Reparto<\/option>\n              <option>Otro<\/option>\n              <option>Tio<\/option>\n              <option>Tia<\/option>\n            <\/select>\n          <\/div>\n        <\/div>\n\n        <div class=\"sec-actions\">\n          <button id=\"secBtnLlamar\" type=\"button\">PULSA AQU\u00cd PARA LLAMAR<\/button>\n          <button id=\"secBtnCerrarSesion2\" class=\"secondary\" type=\"button\">Cerrar sesi\u00f3n<\/button>\n        <\/div>\n\n        <div class=\"sec-muted\" style=\"margin-top:.6rem\">Haz clic en el bot\u00f3n para poder llamar \ud83d\ude42<\/div>\n      <\/section>\n\n      <div id=\"secMsg\" class=\"sec-msg sec-hidden\"><\/div>\n\n      <audio id=\"secSndEnviado\" preload=\"auto\">\n        <source src=\"\/wp-content\/uploads\/sonidos\/enviado.mp3\" type=\"audio\/mpeg\">\n      <\/audio>\n    <\/div>\n\n    <script>\n    (() => {\n      const ajaxUrl = \"https:\\\/\\\/servidorencasa.com\\\/wp-admin\\\/admin-ajax.php\";\n      const THEME_KEY = 'timbre_theme_wp';\n      const SKEY = 'timbre_sesion_wp';\n\n      const wrap = document.getElementById('sec-timbre-wrap');\n      const themeBtn = document.getElementById('secThemeToggle');\n      const msgBox = document.getElementById('secMsg');\n\n      const pantalla1 = document.getElementById('secPantalla1');\n      const pantalla2 = document.getElementById('secPantalla2');\n\n      const codigo = document.getElementById('secCodigo');\n      const pin = document.getElementById('secPin');\n      const nombre = document.getElementById('secNombre');\n      const relacion = document.getElementById('secRelacion');\n\n      const nombre2 = document.getElementById('secNombre2');\n      const relacion2 = document.getElementById('secRelacion2');\n\n      const btnRegistro = document.getElementById('secBtnRegistro');\n      const btnLlamar = document.getElementById('secBtnLlamar');\n      const btnCerrar1 = document.getElementById('secBtnCerrarSesion1');\n      const btnCerrar2 = document.getElementById('secBtnCerrarSesion2');\n\n      const snd = document.getElementById('secSndEnviado');\n\n      let CFG = { lat: 0, lon: 0, radio_m: 250 };\n\n      function applyTheme(mode){\n        wrap.setAttribute('data-theme', mode);\n        localStorage.setItem(THEME_KEY, mode);\n        themeBtn.textContent = (mode === 'light') ? '\u2600\ufe0f' : '\ud83c\udf19';\n      }\n\n      applyTheme(localStorage.getItem(THEME_KEY) || 'dark');\n\n      themeBtn.addEventListener('click', () => {\n        applyTheme(wrap.getAttribute('data-theme') === 'light' ? 'dark' : 'light');\n      });\n\n      function setMsg(txt, type='info'){\n        msgBox.textContent = txt;\n        msgBox.className = 'sec-msg ' + type;\n        msgBox.classList.remove('sec-hidden');\n      }\n\n      function clearMsg(){\n        msgBox.className = 'sec-msg sec-hidden';\n        msgBox.textContent = '';\n      }\n\n      function lock(btn, text='Enviando\u2026'){\n        if(!btn) return;\n        if(!btn.dataset.oldText) btn.dataset.oldText = btn.textContent;\n        btn.disabled = true;\n        btn.textContent = text;\n      }\n\n      function unlock(btn){\n        if(!btn) return;\n        btn.disabled = false;\n        btn.textContent = btn.dataset.oldText || btn.textContent;\n      }\n\n      function show(n){\n        if(n === 1){\n          pantalla1.classList.add('active');\n          pantalla2.classList.remove('active');\n        } else {\n          pantalla2.classList.add('active');\n          pantalla1.classList.remove('active');\n        }\n        clearMsg();\n      }\n\n      function loadS(){\n        try { return JSON.parse(localStorage.getItem(SKEY) || '{}'); }\n        catch { return {}; }\n      }\n\n      function saveS(s){\n        localStorage.setItem(SKEY, JSON.stringify(s));\n      }\n\n      function endS(){\n        localStorage.removeItem(SKEY);\n        location.reload();\n      }\n\n      btnCerrar1.addEventListener('click', endS);\n      btnCerrar2.addEventListener('click', endS);\n\n      async function prepareAudio(){\n        try{\n          if(!snd) return;\n          snd.muted = true;\n          snd.volume = 0;\n          await snd.play().catch(()=>{});\n          snd.pause();\n          snd.currentTime = 0;\n          snd.muted = false;\n          snd.volume = 1;\n        }catch(e){}\n      }\n\n      document.body.addEventListener('click', prepareAudio, { once:true });\n\n      async function playEnviado(){\n        try{\n          if(!snd) return;\n          snd.currentTime = 0;\n          await snd.play();\n        }catch(e){}\n      }\n\n      function toRad(d){\n        return d * Math.PI \/ 180;\n      }\n\n      function distanceMeters(lat1, lon1, lat2, lon2){\n        const R = 6371000;\n        const dLat = toRad(lat2 - lat1);\n        const dLon = toRad(lon2 - lon1);\n        const a =\n          Math.sin(dLat\/2) ** 2 +\n          Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon\/2) ** 2;\n        return 2 * R * Math.asin(Math.sqrt(a));\n      }\n\n      function getPosition(opts = { enableHighAccuracy:true, timeout:10000, maximumAge:0 }){\n        return new Promise((resolve, reject) => {\n          navigator.geolocation.getCurrentPosition(resolve, reject, opts);\n        });\n      }\n\n      async function cargarConfigGPS(codigoActual = ''){\n        try{\n          const url = new URL(ajaxUrl, window.location.origin);\n          url.searchParams.set('action', 'sec_gps_config');\n          if (codigoActual) {\n            url.searchParams.set('codigo', codigoActual);\n          }\n          url.searchParams.set('ts', Date.now());\n\n          const r = await fetch(url.toString(), { cache:'no-store' });\n          const j = await r.json();\n\n          if(j && j.ok){\n            CFG.lat = Number(j.lat || 0);\n            CFG.lon = Number(j.lon || 0);\n            CFG.radio_m = Number(j.radio_m || 250);\n          }\n        }catch(e){}\n      }\n\n      async function validarGPS(){\n        if(!('geolocation' in navigator)){\n          setMsg('Este dispositivo no tiene GPS.', 'err');\n          return false;\n        }\n\n        if(!CFG.lat || !CFG.lon){\n          setMsg('GPS no configurado en el servidor.', 'err');\n          return false;\n        }\n\n        setMsg('Comprobando ubicaci\u00f3n\u2026', 'info');\n\n        const hardTimeout = new Promise((_, reject) =>\n          setTimeout(() => reject(new Error('timeout')), 12000)\n        );\n\n        try{\n          const pos = await Promise.race([getPosition(), hardTimeout]);\n\n          const dist = distanceMeters(\n            pos.coords.latitude,\n            pos.coords.longitude,\n            CFG.lat,\n            CFG.lon\n          );\n\n          if(dist > CFG.radio_m){\n            setMsg('Est\u00e1s fuera del \u00e1rea permitida.', 'err');\n            return false;\n          }\n\n          clearMsg();\n          return true;\n\n        }catch(e){\n          setMsg('No pude obtener tu ubicaci\u00f3n. Activa GPS y reintenta.', 'err');\n          return false;\n        }\n      }\n\n      function normRelacion(v){\n        v = (v || '').trim();\n        if(!v) return '';\n        return v;\n      }\n\n      async function postConTimeout(bodyParams, ms=15000){\n        const ctrl = new AbortController();\n        const t = setTimeout(() => ctrl.abort(), ms);\n\n        try{\n          const res = await fetch(ajaxUrl, {\n            method: 'POST',\n            headers: { 'Content-Type':'application\/x-www-form-urlencoded' },\n            body: bodyParams,\n            cache: 'no-store',\n            signal: ctrl.signal\n          });\n\n          return await res.json();\n        } finally {\n          clearTimeout(t);\n        }\n      }\n\n      (async function init(){\n        const s = loadS();\n        const codigoSesion = s.codigo || '';\n\n        await cargarConfigGPS(codigoSesion);\n\n        if(s.confirmado){\n          nombre2.value = s.nombre || '';\n          relacion2.value = s.relacion || 'Vecino';\n          show(2);\n        } else {\n          if(s.codigo) codigo.value = s.codigo;\n          if(s.pin) pin.value = s.pin;\n          if(s.nombre) nombre.value = s.nombre;\n          if(s.relacion) relacion.value = s.relacion;\n          show(1);\n        }\n      })();\n\n      btnRegistro.addEventListener('click', async () => {\n        clearMsg();\n\n        const c = (codigo.value || '').trim();\n        const p = (pin.value || '').trim();\n        const n = (nombre.value || '').trim();\n        const r = normRelacion(relacion.value || '');\n\n        if(!c || !p || !n || !r){\n          setMsg('Para poder llamar necesito que escribas todos los datos.', 'err');\n          return;\n        }\n\n        if(!\/^\\d{4}$\/.test(p)){\n          setMsg('El PIN debe tener 4 cifras.', 'err');\n          return;\n        }\n\n        lock(btnRegistro, 'Enviando\u2026');\n        setMsg('Registrando\u2026', 'info');\n\n        try{\n          await cargarConfigGPS(c);\n\n          const params = new URLSearchParams({\n            action: 'sec_registrar_llamada',\n            codigo: c,\n            pin: p,\n            nombre: n,\n            relacion: r\n          }).toString();\n\n          const j = await postConTimeout(params, 15000);\n\n          if(j && j.ok){\n            saveS({\n              confirmado: true,\n              codigo: c,\n              pin: p,\n              nombre: n,\n              relacion: r,\n              slug: j.slug || ''\n            });\n\n            nombre2.value = n;\n            relacion2.value = r || 'Vecino';\n            show(2);\n            setMsg('\u2705 Registro correcto. Ya puedes llamar al timbre.', 'ok');\n            return;\n          }\n\n          setMsg((j && j.msg) ? j.msg : 'Error del servidor.', 'err');\n\n        }catch(e){\n          setMsg('Error de conexi\u00f3n o timeout. Reintenta.', 'err');\n        } finally {\n          unlock(btnRegistro);\n        }\n      });\n\n      btnLlamar.addEventListener('click', async () => {\n        clearMsg();\n        lock(btnLlamar, 'Enviando\u2026');\n\n        try{\n          const s = loadS();\n\n          if(!s.confirmado || !s.codigo || !s.pin){\n            setMsg('Falta la sesi\u00f3n. Vuelve a registrarte.', 'err');\n            show(1);\n            unlock(btnLlamar);\n            return;\n          }\n\n          const n = (nombre2.value || '').trim();\n          const r = normRelacion(relacion2.value || '');\n\n          if(!n || !r){\n            setMsg('Pon tu nombre y qui\u00e9n eres.', 'err');\n            unlock(btnLlamar);\n            return;\n          }\n\n          await cargarConfigGPS(s.codigo);\n\n          const okGPS = await validarGPS();\n          if(!okGPS){\n            unlock(btnLlamar);\n            return;\n          }\n\n          const params = new URLSearchParams({\n            action: 'sec_registrar_llamada',\n            codigo: s.codigo,\n            pin: s.pin,\n            nombre: n,\n            relacion: r\n          }).toString();\n\n          const j = await postConTimeout(params, 15000);\n\n          if(j && j.ok){\n            await playEnviado();\n\n            const mensaje = j.msg || 'Llamada correcta.';\n            const tipo = (j.sospechoso === true) ? 'warn' : 'ok';\n            setMsg(mensaje, tipo);\n\n            saveS({ ...s, nombre:n, relacion:r, slug: j.slug || s.slug || '' });\n            unlock(btnLlamar);\n            return;\n          }\n\n          setMsg((j && j.msg) ? j.msg : 'El servidor no devolvi\u00f3 una respuesta v\u00e1lida.', 'err');\n          unlock(btnLlamar);\n\n        }catch(e){\n          setMsg('Error de conexi\u00f3n o timeout. Reintenta.', 'err');\n          unlock(btnLlamar);\n        }\n      });\n\n    })();\n    <\/script>\n    \n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-15","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/servidorencasa.com\/index.php\/wp-json\/wp\/v2\/pages\/15","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/servidorencasa.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/servidorencasa.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/servidorencasa.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/servidorencasa.com\/index.php\/wp-json\/wp\/v2\/comments?post=15"}],"version-history":[{"count":1,"href":"https:\/\/servidorencasa.com\/index.php\/wp-json\/wp\/v2\/pages\/15\/revisions"}],"predecessor-version":[{"id":16,"href":"https:\/\/servidorencasa.com\/index.php\/wp-json\/wp\/v2\/pages\/15\/revisions\/16"}],"wp:attachment":[{"href":"https:\/\/servidorencasa.com\/index.php\/wp-json\/wp\/v2\/media?parent=15"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}