<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hello, world</title>
</head>
<body>
<main>
<h1>Hello, world.</h1>
<p>Every good idea starts somewhere.</p>
</main>
</body>
</html>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#10121b">
<title>SergiolandPhone</title>
<style>
:root {
--bg: #10121b;
--panel: #1b1e2b;
--panel2: #25293a;
--text: #f7f8ff;
--muted: #a5abc0;
--red: #e83b49;
--blue: #6387ff;
--green: #40d69a;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(ellipse at top, #29304a 0, #10121b 60%);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
}
button, input, textarea, select { font: inherit; }
button { cursor: pointer; }
button:focus-visible, input:focus-visible,
textarea:focus-visible, select:focus-visible {
outline: 2px solid #91aaff;
outline-offset: 3px;
}
button:disabled { opacity: .5; cursor: not-allowed; }
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 22px;
border-bottom: 1px solid #ffffff15;
}
.brand { font-weight: 850; letter-spacing: -.5px; }
.brand span { color: #92aaff; }
.status {
color: var(--muted);
font-size: 12px;
display: flex;
align-items: center;
gap: 7px;
}
.dot {
width: 8px; height: 8px; border-radius: 50%;
background: #8c91a5;
}
.dot.online { background: var(--green); }
.layout {
display: grid;
grid-template-columns: minmax(290px, 410px) minmax(260px, 420px);
justify-content: center;
align-items: start;
gap: 30px;
padding: 28px 18px 50px;
}
.phone {
width: 100%;
max-width: 390px;
margin: auto;
padding: 10px;
background: #050609;
border: 1px solid #525564;
border-radius: 42px;
box-shadow: 0 25px 90px #0009;
}
.phone-screen {
min-height: 720px;
height: min(780px, calc(100vh - 100px));
min-height: 620px;
position: relative;
overflow: hidden;
border-radius: 33px;
background: linear-gradient(150deg, #283b70, #171a32 54%, #10121b);
}
.island {
position: absolute;
top: 9px;
left: 50%;
transform: translateX(-50%);
z-index: 5;
width: 94px;
height: 25px;
border-radius: 20px;
background: #050609;
}
.statusbar {
display: flex;
justify-content: space-between;
padding: 13px 20px 8px;
font-size: 12px;
position: relative;
z-index: 4;
}
.screen {
display: none;
height: calc(100% - 42px);
overflow-y: auto;
padding: 12px 17px 24px;
}
.screen.active { display: block; }
.lock {
text-align: center;
padding-top: 90px;
}
.lock .date { color: #d5ddff; font-size: 14px; }
.lock .clock { font-size: 68px; font-weight: 250; letter-spacing: -4px; }
.lock .locknote { color: #e1e6ff; margin-top: 12px; }
.unlock {
margin-top: 90px;
border: 1px solid #ffffff55;
color: white;
background: #ffffff18;
padding: 13px 24px;
border-radius: 30px;
}
.small { color: var(--muted); font-size: 12px; }
.home-top {
display: flex;
justify-content: space-between;
align-items: center;
margin: 18px 0 26px;
}
.home-top h2 { margin: 0; font-size: 23px; }
.weather {
border: 1px solid #ffffff20;
background: #ffffff12;
padding: 10px 12px;
border-radius: 15px;
font-size: 12px;
}
.apps {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 22px 12px;
}
.app {
border: 0;
background: transparent;
color: white;
text-align: center;
min-width: 0;
}
.app-icon {
width: 60px; height: 60px;
border-radius: 19px;
margin: 0 auto 7px;
display: grid;
place-items: center;
font-size: 28px;
box-shadow: inset 0 1px #ffffff30;
}
.app-label { font-size: 12px; }
.icon-alert { background: linear-gradient(140deg, #ff5a61, #a60f30); }
.icon-chat { background: linear-gradient(140deg, #53e6ad, #098d83); }
.icon-phone { background: linear-gradient(140deg, #68e4a5, #17995c); }
.icon-camera { background: linear-gradient(140deg, #a8b1c8, #525b73); }
.icon-gallery { background: linear-gradient(140deg, #ffa66b, #dc537c); }
.icon-settings { background: linear-gradient(140deg, #7888a9, #39415b); }
.icon-browser { background: linear-gradient(140deg, #64b7ff, #4b64e8); }
.icon-notes { background: linear-gradient(140deg, #ffe48b, #d89b35); }
.icon-calendar { background: linear-gradient(140deg, #ff7777, #bc2e45); }
.dock {
display: flex;
justify-content: space-around;
margin-top: 30px;
padding: 13px 6px;
border-radius: 25px;
background: #ffffff18;
}
.dock .app-icon { width: 49px; height: 49px; font-size: 23px; }
.page-title {
display: flex;
align-items: center;
gap: 10px;
margin: 8px 0 20px;
}
.page-title h2 { font-size: 21px; margin: 0; }
.back {
border: 0; color: white; background: #ffffff19;
border-radius: 12px; padding: 9px 12px;
}
.card {
border: 1px solid #ffffff18;
background: #111625b8;
border-radius: 19px;
padding: 15px;
margin-bottom: 13px;
}
.card h3 { margin: 0 0 8px; font-size: 15px; }
.card p { margin: 6px 0; }
.field { display: block; margin: 14px 0; }
.field > span {
display: block; font-size: 12px;
color: #c4c9dc; margin-bottom: 7px;
}
.field input, .field textarea, .field select {
display: block;
width: 100%;
border: 1px solid #41485e;
border-radius: 12px;
padding: 12px;
background: #0d1120;
color: white;
}
.field textarea { min-height: 110px; resize: vertical; }
.primary, .secondary, .danger {
border: 0; border-radius: 12px; padding: 12px 15px;
color: white; width: 100%; font-weight: 750;
}
.primary { background: linear-gradient(100deg, #ed3b4c, #b71d37); }
.secondary { background: #ffffff18; border: 1px solid #ffffff20; }
.danger { background: #6e1c2a; }
.notice {
font-size: 12px; color: #f2d9a4;
background: #b781241a;
border: 1px solid #b7812455;
border-radius: 12px;
padding: 11px;
line-height: 1.5;
}
.connection { color: var(--green); font-size: 12px; }
.alert-history {
border-left: 3px solid var(--red);
padding: 10px 12px;
margin: 10px 0;
background: #ffffff09;
border-radius: 0 12px 12px 0;
overflow-wrap: anywhere;
}
.alert-history strong { display: block; }
.alert-history time { font-size: 11px; color: var(--muted); }
.bottom-home {
width: 100%; margin-top: 14px;
padding: 12px; border: 0; border-radius: 14px;
color: white; background: #ffffff13;
}
.side { padding-top: 10px; }
.side h1 { font-size: 27px; line-height: 1.15; margin: 10px 0; }
.side > p { color: #bbc2d8; line-height: 1.6; }
.side .card { background: #ffffff09; }
.side ul { padding-left: 20px; color: #d1d6e8; line-height: 1.9; }
.side .card p { color: #c3c9dc; line-height: 1.55; }
.overlay {
display: none;
position: fixed;
inset: 0;
z-index: 50;
overflow-y: auto;
background: #a90920;
color: white;
padding: 25px 20px 35px;
}
.overlay.show { display: block; }
.overlay-inner {
max-width: 540px; margin: auto;
min-height: calc(100vh - 60px);
display: flex; flex-direction: column;
justify-content: center;
}
.alert-symbol { font-size: 58px; }
.sim-label {
display: inline-block;
width: fit-content;
border: 1px solid #ffffff9c;
background: #ffffff18;
border-radius: 6px;
padding: 5px 8px;
font-size: 11px; font-weight: 800; letter-spacing: 1px;
}
.overlay h1 { font-size: clamp(30px, 7vw, 46px); line-height: 1.05; }
.overlay .message {
white-space: pre-wrap; overflow-wrap: anywhere;
font-size: 18px; line-height: 1.6;
}
.overlay .meta { font-size: 13px; color: #ffe0e3; }
.overlay button {
background: white; color: #a90920; border: 0;
border-radius: 14px; padding: 15px; font-weight: 850;
margin-top: 20px;
}
.overlay .sound-btn {
background: #780a1b; color: white;
border: 1px solid #ffffff70;
}
#toast {
position: fixed; bottom: 18px; left: 50%;
transform: translateX(-50%);
z-index: 100; display: none;
max-width: 90vw; padding: 12px 16px;
border-radius: 14px; background: #f1f2fa;
color: #171a26; box-shadow: 0 5px 30px #0005;
font-size: 13px;
}
#toast.show { display: block; }
@media (max-width: 760px) {
header { padding: 13px 15px; }
.layout { display: block; padding: 12px 10px 30px; }
.phone { max-width: 410px; }
.phone-screen { height: min(790px, 82dvh); min-height: 610px; }
.side { max-width: 410px; margin: 30px auto; }
}
@media (prefers-reduced-motion: no-preference) {
.app-icon { transition: transform .15s; }
.app:active .app-icon { transform: scale(.94); }
}
</style>
</head>
<body>
<header>
<div class="brand">Sergioland<span>Phone</span></div>
<div class="status">
<span id="headerDot" class="dot"></span>
<span id="headerStatus">Conectando…</span>
</div>
</header>
<main class="layout">
<section class="phone" aria-label="Móvil virtual">
<div class="phone-screen">
<div class="island"></div>
<div class="statusbar">
<span id="phoneTime">09:41</span>
<span>● ▰ ▰ 100%</span>
</div>
<!-- Pantalla de bloqueo -->
<section id="lockScreen" class="screen active">
<div class="lock">
<div class="date" id="lockDate">Cargando fecha…</div>
<div class="clock" id="lockClock">09:41</div>
<div class="locknote">◈ SergiolandPhone</div>
<button class="unlock" id="unlockBtn">Desbloquear ↑</button>
<p class="small">Toca para entrar al móvil</p>
</div>
</section>
<!-- Inicio -->
<section id="homeScreen" class="screen">
<div class="home-top">
<div>
<div class="small">BIENVENIDO A</div>
<h2>SergiolandPhone</h2>
</div>
<div class="weather">📶 <span id="homeConnection">Offline</span></div>
</div>
<div class="card">
<h3>🚨 Centro de emergencias</h3>
<p class="small">Avisos simulados recibidos en este móvil.</p>
<button class="secondary" data-open="alerts">Abrir ES-ALERT →</button>
</div>
<div class="apps">
<button class="app" data-open="alerts">
<span class="app-icon icon-alert">🚨</span>
<span class="app-label">ES-ALERT</span>
</button>
<button class="app" data-open="messages">
<span class="app-icon icon-chat">💬</span>
<span class="app-label">Mensajes</span>
</button>
<button class="app" data-open="phoneApp">
<span class="app-icon icon-phone">☎️</span>
<span class="app-label">Teléfono</span>
</button>
<button class="app" data-open="camera">
<span class="app-icon icon-camera">📷</span>
<span class="app-label">Cámara</span>
</button>
<button class="app" data-open="gallery">
<span class="app-icon icon-gallery">🌄</span>
<span class="app-label">Galería</span>
</button>
<button class="app" data-open="settings">
<span class="app-icon icon-settings">⚙️</span>
<span class="app-label">Ajustes</span>
</button>
<button class="app" data-open="browser">
<span class="app-icon icon-browser">🌐</span>
<span class="app-label">Navegador</span>
</button>
<button class="app" data-open="notes">
<span class="app-icon icon-notes">📝</span>
<span class="app-label">Notas</span>
</button>
<button class="app" data-open="calendar">
<span class="app-icon icon-calendar">📅</span>
<span class="app-label">Calendario</span>
</button>
</div>
<div class="dock">
<button class="app" data-open="phoneApp" aria-label="Teléfono">
<span class="app-icon icon-phone">☎️</span>
</button>
<button class="app" data-open="messages" aria-label="Mensajes">
<span class="app-icon icon-chat">💬</span>
</button>
<button class="app" data-open="camera" aria-label="Cámara">
<span class="app-icon icon-camera">📷</span>
</button>
<button class="app" data-open="settings" aria-label="Ajustes">
<span class="app-icon icon-settings">⚙️</span>
</button>
</div>
</section>
<!-- Aplicación ES-ALERT -->
<section id="alertsScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button>
<h2>ES-ALERT</h2>
</div>
<div class="notice">
SIMULADOR NO OFICIAL. Los avisos se envían únicamente a los
usuarios conectados a esta aplicación. No son alertas de Protección Civil.
</div>
<div class="card" style="margin-top:14px">
<h3>Estado del sistema</h3>
<p id="alertConnection" class="connection">Conectando al servidor…</p>
<p class="small">Los demás usuarios conectados recibirán el aviso.</p>
</div>
<form id="alertForm" class="card">
<h3>Redactar aviso de prueba</h3>
<label class="field">
<span>Título de la alerta</span>
<input id="alertTitle" maxlength="100"
placeholder="Ej. Aviso meteorológico de prueba" required>
</label>
<label class="field">
<span>Zona o municipio ficticio</span>
<input id="alertZone" maxlength="150"
placeholder="Ej. Municipio de prueba" required>
</label>
<label class="field">
<span>Mensaje de emergencia</span>
<textarea id="alertMessage" maxlength="1500"
placeholder="Escribe aquí el aviso simulado…" required></textarea>
</label>
<button class="primary" id="sendBtn" type="submit">
🚨 Enviar alerta de prueba
</button>
<p class="small">Se enviará a otros móviles conectados, no al emisor.</p>
</form>
<div class="card">
<h3>Historial de alertas recibidas</h3>
<div id="alertHistory">
<p class="small">Todavía no has recibido alertas.</p>
</div>
</div>
</section>
<!-- Aplicaciones de demostración -->
<section id="messagesScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button><h2>Mensajes</h2>
</div>
<div class="card">
<h3>💬 Mensajes de Sergioland</h3>
<p class="small">Esta aplicación es una demostración visual.</p>
</div>
</section>
<section id="phoneAppScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button><h2>Teléfono</h2>
</div>
<div class="card">
<h3>☎️ Teléfono virtual</h3>
<p>Simulación de llamadas. No realiza llamadas reales.</p>
</div>
</section>
<section id="cameraScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button><h2>Cámara</h2>
</div>
<div class="card">
<h3>📷 Cámara</h3>
<p>La cámara real necesita permisos del dispositivo y una conexión segura HTTPS.</p>
<button class="secondary" id="cameraBtn">Activar cámara</button>
<video id="cameraVideo" autoplay playsinline
style="display:none;width:100%;border-radius:12px;margin-top:12px"></video>
<button class="secondary" id="stopCameraBtn" style="display:none;margin-top:10px">
Apagar cámara
</button>
</div>
</section>
<section id="galleryScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button><h2>Galería</h2>
</div>
<div class="card">
<h3>🌄 Galería virtual</h3>
<p>Tu galería estará vacía hasta que añadas contenido.</p>
<label class="field">
<span>Elegir imagen del dispositivo</span>
<input type="file" id="galleryFile" accept="image/*">
</label>
<img id="galleryPreview" alt="Vista previa"
style="display:none;width:100%;border-radius:12px">
</div>
</section>
<section id="settingsScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button><h2>Ajustes</h2>
</div>
<div class="card">
<h3>🔊 Sonido de las alertas</h3>
<p class="small">Puedes activar o silenciar el sonido simulado.</p>
<button class="secondary" id="soundToggle">Sonido activado</button>
<button class="secondary" id="testSound" style="margin-top:10px">
Probar sonido de emergencia
</button>
<p class="small">El navegador puede exigir una interacción para permitir audio.</p>
</div>
<div class="card">
<h3>📱 Acerca del móvil</h3>
<p>SergiolandPhone</p>
<p class="small">Simulador web · Versión 1.0.0</p>
</div>
</section>
<section id="browserScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button><h2>Navegador</h2>
</div>
<div class="card">
<h3>🌐 Navegador virtual</h3>
<p>Es una demostración. Los enlaces externos no se abren dentro de esta aplicación.</p>
</div>
</section>
<section id="notesScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button><h2>Notas</h2>
</div>
<div class="card">
<h3>📝 Mis notas</h3>
<textarea id="notesText" placeholder="Escribe una nota…"
style="width:100%;min-height:180px;background:#0d1120;color:white;border:1px solid #41485e;border-radius:12px;padding:12px"></textarea>
<p class="small">Las notas se guardan solo en esta pestaña mientras esté abierta.</p>
</div>
</section>
<section id="calendarScreen" class="screen">
<div class="page-title">
<button class="back" data-home>←</button><h2>Calendario</h2>
</div>
<div class="card">
<h3>📅 Hoy</h3>
<p id="calendarDate"></p>
<p class="small">Calendario de demostración.</p>
</div>
</section>
</div>
</section>
<aside class="side">
<p class="small">SIMULADOR WEB · SERGIOLAND</p>
<h1>Un móvil conectado a tus alertas. 🚨</h1>
<p>
Envía una alerta de prueba desde un dispositivo y recibe la notificación
en los demás dispositivos conectados al mismo servidor.
</p>
<div class="card">
<h3>📡 Conexión</h3>
<p id="sideConnection">Esperando conexión…</p>
<p class="small">Todos deben abrir la misma dirección pública del servidor.</p>
</div>
<div class="card">
<h3>Cómo probarlo</h3>
<ul>
<li>Inicia el servidor.</li>
<li>Abre la web en dos dispositivos.</li>
<li>Desbloquea los móviles y entra en ES-ALERT.</li>
<li>Envía una alerta de prueba desde uno.</li>
<li>El otro recibirá el aviso y podrá reproducir el sonido.</li>
</ul>
</div>
<div class="notice">
Las alertas de esta web son ficticias. No se conectan al sistema oficial
ES-ALERT ni a los servicios de emergencias.
</div>
</aside>
</main>
<!-- Alerta recibida -->
<div id="alertOverlay" class="overlay" role="dialog"
aria-modal="true" aria-labelledby="receivedTitle">
<div class="overlay-inner">
<div class="alert-symbol">🚨</div>
<div class="sim-label">SIMULACIÓN · NO OFICIAL</div>
<p class="meta" id="receivedZone"></p>
<h1 id="receivedTitle">ALERTA DE PRUEBA</h1>
<div id="receivedMessage" class="message"></div>
<p id="receivedTime" class="meta"></p>
<button id="ackBtn">He leído la alerta</button>
<button id="replaySoundBtn" class="sound-btn">🔊 Repetir sonido</button>
</div>
</div>
<div id="toast" role="status" aria-live="polite"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
(() => {
"use strict";
const $ = id => document.getElementById(id);
const socket = io();
let connected = false;
let soundEnabled = true;
let currentAlert = null;
let audioContext = null;
let cameraStream = null;
const history = [];
function toast(message) {
$("toast").textContent = message;
$("toast").classList.add("show");
clearTimeout(toast.timer);
toast.timer = setTimeout(() => $("toast").classList.remove("show"), 3500);
}
function updateClock() {
const now = new Date();
$("phoneTime").textContent = now.toLocaleTimeString("es-ES", {
hour: "2-digit", minute: "2-digit"
});
$("lockClock").textContent = now.toLocaleTimeString("es-ES", {
hour: "2-digit", minute: "2-digit"
});
$("lockDate").textContent = now.toLocaleDateString("es-ES", {
weekday: "long", day: "numeric", month: "long"
});
$("calendarDate").textContent = now.toLocaleDateString("es-ES", {
weekday: "long", year: "numeric", month: "long", day: "numeric"
});
}
updateClock();
setInterval(updateClock, 15000);
function setConnection(ok, message) {
connected = ok;
$("headerDot").classList.toggle("online", ok);
$("headerStatus").textContent = message;
$("homeConnection").textContent = ok ? "Online" : "Offline";
$("alertConnection").textContent = ok
? "● Conectado al servidor"
: "○ Sin conexión al servidor";
$("sideConnection").textContent = message;
$("alertConnection").style.color = ok ? "#40d69a" : "#ff8994";
}
socket.on("connect", () => setConnection(true, "Conectado"));
socket.on("disconnect", () => setConnection(false, "Desconectado"));
socket.on("connect_error", () =>
setConnection(false, "Error de conexión")
);
socket.on("server-status", data => {
if (data && data.connected) setConnection(true, "Conectado");
});
const screenIds = [
"lockScreen", "homeScreen", "alertsScreen", "messagesScreen",
"phoneAppScreen", "cameraScreen", "galleryScreen", "settingsScreen",
"browserScreen", "notesScreen", "calendarScreen"
];
function showScreen(id) {
screenIds.forEach(screenId => {
$(screenId).classList.toggle("active", screenId === id);
});
}
$("unlockBtn").addEventListener("click", () => showScreen("homeScreen"));
document.querySelectorAll("[data-open]").forEach(button => {
button.addEventListener("click", () => {
const screenMap = {
alerts: "alertsScreen",
messages: "messagesScreen",
phoneApp: "phoneAppScreen",
camera: "cameraScreen",
gallery: "galleryScreen",
settings: "settingsScreen",
browser: "browserScreen",
notes: "notesScreen",
calendar: "calendarScreen"
};
const target = screenMap[button.dataset.open];
if (target) showScreen(target);
});
});
document.querySelectorAll("[data-home]").forEach(button => {
button.addEventListener("click", () => showScreen("homeScreen"));
});
// Sonido de prueba generado por el navegador.
// No es la grabación oficial del sonido ES-ALERT.
async function playEmergencySound() {
if (!soundEnabled) {
toast("El sonido está desactivado en Ajustes.");
return;
}
try {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (!AudioCtx) {
toast("Este navegador no permite reproducir el sonido.");
return;
}
if (!audioContext) audioContext = new AudioCtx();
if (audioContext.state === "suspended") await audioContext.resume();
const start = audioContext.currentTime + 0.05;
// Dos tonos alternos con pausas breves, repetidos dos veces.
const pattern = [
[880, 0.00, 0.42], [660, 0.48, 0.42],
[880, 0.96, 0.42], [660, 1.44, 0.42],
[880, 2.10, 0.42], [660, 2.58, 0.42],
[880, 3.06, 0.42], [660, 3.54, 0.42]
];
for (const [frequency, offset, duration] of pattern) {
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
osc.type = "sine";
osc.frequency.value = frequency;
gain.gain.setValueAtTime(0.0001, start + offset);
gain.gain.exponentialRampToValueAtTime(0.20, start + offset + 0.025);
gain.gain.setValueAtTime(0.20, start + offset + duration - 0.04);
gain.gain.exponentialRampToValueAtTime(
0.0001, start + offset + duration
);
osc.connect(gain);
gain.connect(audioContext.destination);
osc.start(start + offset);
osc.stop(start + offset + duration + 0.02);
}
} catch (error) {
toast("No se pudo reproducir el sonido. Toca «Repetir sonido».");
}
}
$("soundToggle").addEventListener("click", () => {
soundEnabled = !soundEnabled;
$("soundToggle").textContent =
soundEnabled ? "Sonido activado" : "Sonido desactivado";
toast(soundEnabled ? "Sonido activado." : "Sonido silenciado.");
});
$("testSound").addEventListener("click", playEmergencySound);
$("replaySoundBtn").addEventListener("click", playEmergencySound);
function renderHistory() {
const container = $("alertHistory");
container.replaceChildren();
if (!history.length) {
const empty = document.createElement("p");
empty.className = "small";
empty.textContent = "Todavía no has recibido alertas.";
container.appendChild(empty);
return;
}
history.slice().reverse().forEach(alert => {
const item = document.createElement("div");
item.className = "alert-history";
const title = document.createElement("strong");
title.textContent = alert.title;
const zone = document.createElement("p");
zone.textContent = "📍 " + alert.zone;
const message = document.createElement("p");
message.textContent = alert.message;
const time = document.createElement("time");
time.textContent = new Date(alert.timestamp).toLocaleString("es-ES");
item.append(title, zone, message, time);
container.appendChild(item);
});
}
function receiveAlert(alert) {
if (!alert || typeof alert.title !== "string" ||
typeof alert.message !== "string" ||
typeof alert.zone !== "string") return;
currentAlert = alert;
history.push(alert);
if (history.length > 30) history.shift();
renderHistory();
$("receivedZone").textContent = "ZONA: " + alert.zone;
$("receivedTitle").textContent = alert.title;
$("receivedMessage").textContent = alert.message;
$("receivedTime").textContent =
"Recibida: " + new Date(alert.timestamp).toLocaleString("es-ES");
$("alertOverlay").classList.add("show");
document.title = "🚨 Alerta de prueba · SergiolandPhone";
// Intentar sonido; algunos navegadores requieren permiso/interacción previa.
playEmergencySound();
}
socket.on("receive-alert", receiveAlert);
$("ackBtn").addEventListener("click", () => {
$("alertOverlay").classList.remove("show");
document.title = "SergiolandPhone";
});
$("alertForm").addEventListener("submit", event => {
event.preventDefault();
if (!connected) {
toast("No hay conexión con el servidor. Inténtalo de nuevo.");
return;
}
const data = {
title: $("alertTitle").value.trim(),
zone: $("alertZone").value.trim(),
message: $("alertMessage").value.trim()
};
if (!data.title || !data.zone || !data.message) {
toast("Completa todos los campos.");
return;
}
$("sendBtn").disabled = true;
$("sendBtn").textContent = "Enviando…";
socket.timeout(5000).emit("send-alert", data, (error, response) => {
$("sendBtn").disabled = false;
$("sendBtn").textContent = "🚨 Enviar alerta de prueba";
if (error) {
toast("No se ha recibido respuesta del servidor.");
return;
}
if (!response || !response.ok) {
toast(response?.error || "No se pudo enviar la alerta.");
return;
}
toast("Alerta de prueba enviada a los demás móviles conectados.");
$("alertForm").reset();
});
});
// Cámara: requiere HTTPS o localhost y permiso del usuario.
$("cameraBtn").addEventListener("click", async () => {
if (!navigator.mediaDevices?.getUserMedia) {
toast("La cámara requiere HTTPS y permisos del navegador.");
return;
}
try {
cameraStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" }, audio: false
});
$("cameraVideo").srcObject = cameraStream;
$("cameraVideo").style.display = "block";
$("stopCameraBtn").style.display = "block";
$("cameraBtn").style.display = "none";
} catch {
toast("No se pudo activar la cámara. Revisa los permisos.");
}
});
function stopCamera() {
if (cameraStream) cameraStream.getTracks().forEach(track => track.stop());
cameraStream = null;
$("cameraVideo").srcObject = null;
$("cameraVideo").style.display = "none";
$("stopCameraBtn").style.display = "none";
$("cameraBtn").style.display = "block";
}
$("stopCameraBtn").addEventListener("click", stopCamera);
window.addEventListener("pagehide", stopCamera);
$("galleryFile").addEventListener("change", event => {
const file = event.target.files && event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
$("galleryPreview").src = reader.result;
$("galleryPreview").style.display = "block";
};
reader.readAsDataURL(file);
});
// Recibir alertas en primer plano o con la pestaña minimizada.
document.addEventListener("visibilitychange", () => {
if (!document.hidden) document.title = "SergiolandPhone";
});
setConnection(false, "Conectando…");
})();
</script>
</body>
</html>