Files
TimbreESP32/ui/index.html
Angel Ivan 2a9eabce14 Fist commit
2026-10-02 12:08:21 -06:00

398 lines
18 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#123047">
<meta name="color-scheme" content="light">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'">
<title>TimbreESP · Control local</title>
<link rel="stylesheet" href="styles.css">
<script src="app.js" defer></script>
</head>
<body>
<a class="skip-link" href="#contenido">Saltar al contenido</a>
<div id="app" aria-busy="true">
<section id="screen-bootstrap" class="auth-screen" aria-labelledby="bootstrap-title">
<div class="auth-card auth-card--compact">
<div class="brand brand--centered" aria-label="TimbreESP">
<span class="brand-mark" aria-hidden="true">TE</span>
<span>TimbreESP</span>
</div>
<div class="state-block" role="status" aria-live="polite">
<span class="spinner" aria-hidden="true"></span>
<div>
<h1 id="bootstrap-title">Preparando el equipo</h1>
<p id="bootstrap-message">Comprobando el estado de la configuración.</p>
</div>
</div>
<div id="bootstrap-error" class="notice notice--error" role="alert" hidden>
<p id="bootstrap-error-message"></p>
<button id="bootstrap-retry" class="button button--secondary" type="button">Reintentar</button>
</div>
</div>
</section>
<section id="screen-claim" class="auth-screen" aria-labelledby="claim-title" hidden>
<div class="auth-card">
<div class="brand brand--centered" aria-label="TimbreESP">
<span class="brand-mark" aria-hidden="true">TE</span>
<span>TimbreESP</span>
</div>
<div class="auth-heading">
<p class="eyebrow">Configuración inicial</p>
<h1 id="claim-title" tabindex="-1">Protege el equipo</h1>
<p>Usa el token de un solo uso y crea una contraseña para esta interfaz local.</p>
</div>
<form id="claim-form" class="form-stack" novalidate>
<div class="form-field">
<label for="claim-token">Token de configuración</label>
<input id="claim-token" name="token" type="text" required minlength="16" autocomplete="off" autocapitalize="none" spellcheck="false" maxlength="128" placeholder="Introduce el token entregado por el equipo">
<p class="field-help">El token se invalida después de Claim. No se conserva en este navegador.</p>
</div>
<div class="form-grid form-grid--two">
<div class="form-field">
<label for="claim-password">Nueva contraseña</label>
<input id="claim-password" name="password" type="password" required minlength="12" maxlength="128" autocomplete="new-password">
</div>
<div class="form-field">
<label for="claim-password-confirm">Repite la contraseña</label>
<input id="claim-password-confirm" name="passwordConfirm" type="password" required minlength="12" maxlength="128" autocomplete="new-password">
</div>
</div>
<p id="claim-error" class="form-error" role="alert" hidden></p>
<button id="claim-submit" class="button button--primary button--wide" type="submit">Claim y continuar</button>
</form>
</div>
</section>
<section id="screen-login" class="auth-screen" aria-labelledby="login-title" hidden>
<div class="auth-card auth-card--compact">
<div class="brand brand--centered" aria-label="TimbreESP">
<span class="brand-mark" aria-hidden="true">TE</span>
<span>TimbreESP</span>
</div>
<div class="auth-heading">
<p class="eyebrow">Acceso local</p>
<h1 id="login-title" tabindex="-1">Iniciar sesión</h1>
<p id="login-intro">Introduce la contraseña configurada para controlar el timbre.</p>
</div>
<form id="login-form" class="form-stack" novalidate>
<div class="form-field">
<label for="login-password">Contraseña</label>
<input id="login-password" name="password" type="password" required maxlength="128" autocomplete="current-password" autofocus>
</div>
<p id="login-error" class="form-error" role="alert" hidden></p>
<button id="login-submit" class="button button--primary button--wide" type="submit">Entrar</button>
</form>
<p class="privacy-note">La sesión y el token de protección se mantienen únicamente en memoria.</p>
</div>
</section>
<div id="dashboard" hidden>
<header class="topbar">
<div class="topbar__inner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">TE</span>
<span>
<strong>TimbreESP</strong>
<small>Control local</small>
</span>
</div>
<div class="topbar__actions">
<span id="connection-badge" class="connection-badge connection-badge--loading" role="status" aria-live="polite">
<span class="status-indicator" aria-hidden="true"></span>
<span id="connection-text">Conectando</span>
</span>
<button id="logout-button" class="button button--quiet" type="button">Cerrar sesión</button>
</div>
</div>
</header>
<main id="contenido" class="page-shell" tabindex="-1">
<div class="page-heading">
<div>
<p class="eyebrow">Panel operativo</p>
<h1>Estado del equipo</h1>
<p>Configuración, programación y comprobaciones del timbre principal.</p>
</div>
<button id="refresh-data" class="button button--secondary" type="button">Actualizar datos</button>
</div>
<div id="csrf-warning" class="notice notice--warning" role="alert" hidden>
La sesión está iniciada, pero el dispositivo no entregó un token de protección. Las acciones de configuración se bloquearán.
</div>
<section class="overview-grid" aria-label="Resumen del equipo">
<article class="card clock-card" aria-labelledby="clock-title">
<div class="card-heading">
<div>
<p class="eyebrow">Hora del dispositivo</p>
<h2 id="clock-title">Reloj</h2>
</div>
<span id="clock-indicator" class="state-pill state-pill--muted">Sin datos</span>
</div>
<div class="clock-readout" aria-live="off">
<time id="clock-value" datetime="">--:--:--</time>
<p id="clock-date">Esperando estado…</p>
</div>
<p id="clock-status" class="status-message status-message--muted">Consultando la hora.</p>
<div class="card-footer card-footer--actions">
<button id="sync-time-button" class="button button--secondary" type="button">Usar la hora de este dispositivo</button>
</div>
</article>
<article class="card" aria-labelledby="sequence-title">
<div class="card-heading">
<div>
<p class="eyebrow">Salida principal</p>
<h2 id="sequence-title">Ciclo del timbre</h2>
</div>
<span id="sequence-indicator" class="state-pill state-pill--muted">Inactivo</span>
</div>
<dl class="detail-list">
<div>
<dt>Patrón</dt>
<dd id="active-pattern-label">—</dd>
</div>
<div>
<dt>Salida</dt>
<dd>GPIO22</dd>
</div>
</dl>
<p id="state-message" class="status-message status-message--muted">Consultando el estado…</p>
<div class="card-footer">
<button id="stop-button" class="button button--danger" type="button" disabled>Detener secuencia</button>
</div>
</article>
<article class="card" aria-labelledby="hardware-title">
<div class="card-heading">
<div>
<p class="eyebrow">Salidas</p>
<h2 id="hardware-title">Hardware</h2>
</div>
</div>
<ul class="hardware-list">
<li>
<span class="hardware-icon hardware-icon--primary" aria-hidden="true">22</span>
<span><strong>GPIO22</strong><small>Timbre principal</small></span>
</li>
<li>
<span class="hardware-icon" aria-hidden="true">23</span>
<span><strong>GPIO23</strong><small>Reservado; no se controla</small></span>
</li>
</ul>
</article>
</section>
<section class="card section-card" aria-labelledby="profiles-title">
<div class="section-heading">
<div>
<p class="eyebrow">Modos de operación</p>
<h2 id="profiles-title">Perfiles</h2>
<p>Selecciona el perfil activo y organiza distintos modos de horario.</p>
</div>
<button id="add-profile-button" class="button button--primary" type="button">Añadir perfil</button>
</div>
<div class="active-profile-panel">
<form id="active-profile-form" class="active-profile-form">
<div class="form-field">
<label for="active-profile-select">Perfil activo</label>
<select id="active-profile-select" required disabled>
<option value="">Cargando perfiles…</option>
</select>
</div>
<button id="active-profile-submit" class="button button--secondary" type="submit" disabled>Guardar perfil activo</button>
</form>
</div>
<div id="profiles-content" class="resource-content" aria-live="polite" aria-busy="true"></div>
</section>
<section class="card section-card" aria-labelledby="schedules-title">
<div class="section-heading">
<div>
<p class="eyebrow">Automatización</p>
<h2 id="schedules-title">Horarios</h2>
<p>El timbre principal puede tener como máximo 100 horarios entre todos los perfiles.</p>
</div>
<button id="add-schedule-button" class="button button--primary" type="button">Añadir horario</button>
</div>
<div class="schedule-toolbar">
<div id="profile-tabs" class="segmented-control" role="tablist" aria-label="Filtrar horarios por perfil"></div>
<span id="schedule-count" class="count-badge">0 / 100 horarios</span>
</div>
<div id="schedules-content" class="resource-content" aria-live="polite" aria-busy="true"></div>
</section>
<section class="card section-card" aria-labelledby="patterns-title">
<div class="section-heading">
<div>
<p class="eyebrow">Secuencias electricas</p>
<h2 id="patterns-title">Patrones</h2>
<p>Cada patrón alterna seis fases: encendido, apagado, encendido, apagado, encendido y apagado.</p>
</div>
<button id="add-pattern-button" class="button button--primary" type="button">Añadir patrón</button>
</div>
<div class="hardware-note">
<strong>Destino:</strong> GPIO22 (principal). GPIO23 está reservado y no se puede seleccionar.
</div>
<div id="patterns-content" class="resource-content" aria-live="polite" aria-busy="true"></div>
</section>
<section class="card section-card" aria-labelledby="holidays-title">
<div class="section-heading">
<div>
<p class="eyebrow">Excepciones anuales</p>
<h2 id="holidays-title">Feriados</h2>
<p>Las fechas de silencio se repiten cada año, sin guardar un año concreto.</p>
</div>
</div>
<form id="holiday-form" class="holiday-form" novalidate>
<div class="form-field">
<label for="holiday-month">Mes</label>
<select id="holiday-month" name="month" required></select>
</div>
<div class="form-field">
<label for="holiday-day">Día</label>
<input id="holiday-day" name="day" type="number" min="1" max="31" step="1" value="1" required inputmode="numeric">
</div>
<button id="holiday-submit" class="button button--primary" type="submit">Añadir feriado</button>
</form>
<p id="holiday-error" class="form-error" role="alert" hidden></p>
<div id="holidays-content" class="resource-content" aria-live="polite" aria-busy="true"></div>
</section>
</main>
<footer class="page-footer">
<p>Interfaz local de TimbreESP. No se carga ningún recurso desde Internet.</p>
</footer>
</div>
</div>
<dialog id="profile-dialog" class="dialog" aria-labelledby="profile-dialog-title">
<form id="profile-form" class="dialog__body" novalidate>
<div class="dialog__header">
<div>
<p class="eyebrow">Perfil</p>
<h2 id="profile-dialog-title">Añadir perfil</h2>
</div>
<button class="icon-button" type="button" data-close-dialog aria-label="Cerrar">×</button>
</div>
<input id="profile-id" name="id" type="hidden">
<div class="form-field">
<label for="profile-name">Nombre del perfil</label>
<input id="profile-name" name="name" type="text" required maxlength="32" autocomplete="off" placeholder="Ej. horario de invierno">
</div>
<p id="profile-form-error" class="form-error" role="alert" hidden></p>
<div class="dialog__actions">
<button class="button button--quiet" type="button" data-close-dialog>Cancelar</button>
<button id="profile-submit" class="button button--primary" type="submit">Guardar perfil</button>
</div>
</form>
</dialog>
<dialog id="schedule-dialog" class="dialog dialog--wide" aria-labelledby="schedule-dialog-title">
<form id="schedule-form" class="dialog__body" novalidate>
<div class="dialog__header">
<div>
<p class="eyebrow">Programación</p>
<h2 id="schedule-dialog-title">Añadir horario</h2>
</div>
<button class="icon-button" type="button" data-close-dialog aria-label="Cerrar">×</button>
</div>
<input id="schedule-id" name="id" type="hidden">
<div class="form-grid form-grid--two">
<div class="form-field">
<label for="schedule-time">Hora</label>
<input id="schedule-time" name="time" type="time" required>
</div>
<div class="form-field">
<label for="schedule-profile">Perfil</label>
<select id="schedule-profile" name="profileId" required></select>
</div>
</div>
<div class="form-field">
<label for="schedule-pattern">Patrón del timbre</label>
<select id="schedule-pattern" name="patternId" required></select>
</div>
<fieldset class="days-fieldset">
<legend>Días de la semana</legend>
<p class="field-help">Se envía como bitmask: domingo es el bit 0 y sábado el bit 6.</p>
<div id="schedule-days" class="day-picker"></div>
</fieldset>
<p id="schedule-form-error" class="form-error" role="alert" hidden></p>
<div class="dialog__actions">
<button class="button button--quiet" type="button" data-close-dialog>Cancelar</button>
<button id="schedule-submit" class="button button--primary" type="submit">Guardar horario</button>
</div>
</form>
</dialog>
<dialog id="pattern-dialog" class="dialog dialog--wide" aria-labelledby="pattern-dialog-title">
<form id="pattern-form" class="dialog__body" novalidate>
<div class="dialog__header">
<div>
<p class="eyebrow">Ciclo eléctrico</p>
<h2 id="pattern-dialog-title">Añadir patrón</h2>
</div>
<button class="icon-button" type="button" data-close-dialog aria-label="Cerrar">×</button>
</div>
<input id="pattern-id" name="id" type="hidden">
<div class="form-grid form-grid--two">
<div class="form-field">
<label for="pattern-code">Código</label>
<select id="pattern-code" name="legacyCode" required>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
<p class="field-help">Código heredado que identifica el patrón.</p>
</div>
<div class="form-field">
<span class="field-label">Destino</span>
<div class="readonly-value">GPIO22 · principal</div>
<p class="field-help">GPIO23 está reservado.</p>
</div>
</div>
<fieldset class="phases-fieldset">
<legend>Fases del ciclo</legend>
<div class="phase-grid" id="pattern-phases"></div>
</fieldset>
<label class="checkbox-field checkbox-field--standalone" for="pattern-enabled">
<input id="pattern-enabled" name="enabled" type="checkbox">
<span>Patrón habilitado</span>
</label>
<p id="pattern-form-error" class="form-error" role="alert" hidden></p>
<div class="dialog__actions">
<button class="button button--quiet" type="button" data-close-dialog>Cancelar</button>
<button id="pattern-submit" class="button button--primary" type="submit">Guardar patrón</button>
</div>
</form>
</dialog>
<dialog id="confirm-dialog" class="dialog dialog--confirm" aria-labelledby="confirm-title" aria-describedby="confirm-message">
<div class="dialog__body">
<div class="dialog__header">
<div>
<p class="eyebrow">Confirmar acción</p>
<h2 id="confirm-title">Confirmar</h2>
</div>
<button class="button button--quiet" type="button" data-close-dialog aria-label="Cerrar">Cerrar</button>
</div>
<p id="confirm-message" class="confirm-message"></p>
<div class="dialog__actions">
<button class="button button--secondary" type="button" data-close-dialog>Cancelar</button>
<button id="confirm-action-button" class="button button--danger" type="button">Confirmar</button>
</div>
</div>
</dialog>
<div id="toast-region" class="toast-region" aria-live="polite" aria-atomic="false"></div>
<noscript>
<div class="noscript-notice">Esta interfaz necesita JavaScript para comunicarse con TimbreESP.</div>
</noscript>
</body>
</html>