feat(telegram): Mini App estatica para elegir la zona vigilada
All checks were successful
build-image / test (push) Successful in 2m39s
build-image / build (push) Successful in 11m47s

La sirve la web en /telegram/ y la abre el bot con un boton web_app (Bot API 6.0).
Mapa con marcador arrastrable y circulo de radio, y devuelve {lat,lon,km} con
WebApp.sendData(). Sin backend: no habla con Meteor ni necesita sesion. Leaflet
vendorizado en public/telegram/vendor para no depender de una CDN.

Sustituye al flujo de cinco pasos del bot de 2018. Detalle en
tcef-notifications/docs/legacy-bot.md §10.
This commit is contained in:
vjrj 2026-08-04 07:40:55 +02:00
parent bba2ad5523
commit 23ac2ab99b
8 changed files with 796 additions and 0 deletions

129
public/telegram/index.html Normal file
View file

@ -0,0 +1,129 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Zona a vigilar</title>
<!--
Mini App de Telegram para elegir la zona vigilada (fase 8).
Sustituye al flujo de cinco pasos del bot de 2018 (compartir ubicación o escribir el pueblo →
geocodificación de Google → mapa de confirmación → menú de cinco radios fijos). Aquí se marca
el punto en el mapa y se elige el radio que se quiera.
Es una página ESTÁTICA a propósito: no habla con el servidor de la web ni necesita sesión. El
resultado se devuelve con `Telegram.WebApp.sendData()`, que Telegram entrega al bot como un
mensaje de servicio del propio chat — así que no hay que validar `initData` en ningún sitio ni
exponer un endpoint nuevo. Leaflet va vendorizado en ./vendor para no depender de una CDN
(la del bot puede abrirse desde redes con poca cobertura).
-->
<link rel="stylesheet" href="vendor/leaflet.css">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script src="vendor/leaflet.js"></script>
<style>
:root { color-scheme: light dark; }
html, body { margin: 0; height: 100%; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
body { display: flex; flex-direction: column; background: var(--tg-theme-bg-color, #fff); color: var(--tg-theme-text-color, #000); }
#map { flex: 1 1 auto; min-height: 0; }
.panel { padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); display: grid; gap: 10px; }
.fila { display: flex; align-items: center; gap: 12px; }
.fila label { font-size: 15px; white-space: nowrap; }
#km { flex: 1 1 auto; }
#valor { font-variant-numeric: tabular-nums; font-weight: 600; min-width: 4.5em; text-align: right; }
button { font: inherit; font-weight: 600; padding: 12px; border: 0; border-radius: 10px; cursor: pointer;
background: var(--tg-theme-button-color, #2ea6ff); color: var(--tg-theme-button-text-color, #fff); }
button[disabled] { opacity: .5; cursor: default; }
.ayuda { font-size: 13px; opacity: .7; margin: 0; }
</style>
</head>
<body>
<div id="map"></div>
<div class="panel">
<p class="ayuda" id="ayuda">Toca el mapa o arrastra el marcador para elegir la zona que quieres vigilar.</p>
<div class="fila">
<label for="km">Radio</label>
<input id="km" type="range" min="1" max="100" step="1" value="10">
<span id="valor">10 km</span>
</div>
<button id="enviar" disabled>Guardar zona</button>
</div>
<script>
(function () {
var tg = window.Telegram && window.Telegram.WebApp;
if (tg) { tg.ready(); tg.expand(); }
// El idioma lo dice Telegram; si no, español. Solo hay tres cadenas, no vale la pena i18next.
var LANG = ((tg && tg.initDataUnsafe && tg.initDataUnsafe.user && tg.initDataUnsafe.user.language_code) || 'es').slice(0, 2);
var T = {
es: { ayuda: 'Toca el mapa o arrastra el marcador para elegir la zona que quieres vigilar.', radio: 'Radio', guardar: 'Guardar zona' },
en: { ayuda: 'Tap the map or drag the marker to choose the area you want to watch.', radio: 'Radius', guardar: 'Save area' },
gl: { ayuda: 'Toca o mapa ou arrastra o marcador para escoller a zona que queres vixiar.', radio: 'Raio', guardar: 'Gardar zona' },
pt: { ayuda: 'Toca no mapa ou arrasta o marcador para escolher a zona a vigiar.', radio: 'Raio', guardar: 'Guardar zona' }
}[LANG] || null;
if (T) {
document.getElementById('ayuda').textContent = T.ayuda;
document.querySelector('label[for=km]').textContent = T.radio;
document.getElementById('enviar').textContent = T.guardar;
}
L.Icon.Default.prototype.options.imagePath = 'vendor/images/';
var map = L.map('map', { zoomControl: true }).setView([40.4168, -3.7038], 6);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map);
var marcador = null;
var circulo = null;
var km = Number(document.getElementById('km').value);
var boton = document.getElementById('enviar');
var valor = document.getElementById('valor');
function pinta(latlng) {
if (!marcador) {
marcador = L.marker(latlng, { draggable: true }).addTo(map);
marcador.on('dragend', function () { pinta(marcador.getLatLng()); });
circulo = L.circle(latlng, { radius: km * 1000, color: '#e2504f', weight: 2, fillOpacity: 0.15 }).addTo(map);
} else {
marcador.setLatLng(latlng);
circulo.setLatLng(latlng);
}
circulo.setRadius(km * 1000);
boton.disabled = false;
}
map.on('click', function (e) { pinta(e.latlng); });
document.getElementById('km').addEventListener('input', function (e) {
km = Number(e.target.value);
valor.textContent = km + ' km';
if (circulo) circulo.setRadius(km * 1000);
});
// Arranque: si el navegador da la posición, se centra ahí y se propone ya una zona.
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (pos) {
var ll = L.latLng(pos.coords.latitude, pos.coords.longitude);
map.setView(ll, 11);
pinta(ll);
}, function () { /* sin permiso: se elige a mano */ }, { timeout: 8000 });
}
boton.addEventListener('click', function () {
if (!marcador) return;
var ll = marcador.getLatLng();
var datos = JSON.stringify({ v: 1, lat: Number(ll.lat.toFixed(6)), lon: Number(ll.lng.toFixed(6)), km: km });
if (tg && tg.sendData) {
tg.sendData(datos); // Telegram cierra la ventana y entrega esto al bot
} else {
// Fuera de Telegram (abrir la URL en un navegador normal) no hay a quién enviárselo.
boton.disabled = true;
document.getElementById('ayuda').textContent = 'Esta página solo funciona abierta desde el bot de Telegram.';
}
});
})();
</script>
</body>
</html>