Open Net Worth
Open Net Worth is an open-source personal AI wealth assistant you run yourself: one ledger for your whole financial life, worked by an AI agent you control.
Manage and track the solidarity fund, view balance and receive notifications
AI summary · website content
FinanceJavaScript · HTMLGitHub @33Javier33OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗PWA (Progressive Web App) para socios del Fondo Solidario de Propina del Casino de Puerto Varas. Permite a cada socio consultar su balance, historial de recaudaciones, anticipos, estadísticas y comunicarse con el equipo administrativo.
| Campo | Valor |
|---|---|
| Nombre de la App | Bóveda Personal |
| Short name (PWA) | Bóveda |
| Versión actual | V19 (interna) |
| Idioma | Español |
| Orientación | Portrait |
| Color tema | #001723 (azul marino oscuro) |
| Framework CSS | Tailwind CSS CDN v3 (plugins: forms, container-queries) |
| Fuentes | Inter (cuerpo), Manrope (titulares), Material Symbols Outlined (íconos) |
┌─────────────────────────────────────────────────────┐
│ Dispositivo del Socio │
│ PWA instalada (standalone, portrait) │
│ ┌──────────────────────────────────────────────┐ │
│ │ index.html → app.css + app.js │ │
│ │ Service Worker (sw.js) — cache v12 │ │
│ └────────────────┬─────────────────────────────┘ │
└───────────────────┼─────────────────────────────────┘
│ fetch (interceptado por supabase-api.js)
┌──────────┴──────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Supabase │ │ Supabase │
│ Socios │ │ Recaud. │
│ (teemahk…) │ │ (lpulmj…) │
└─────────────┘ └─────────────┘
Dos proyectos Supabase independientes:
teemahksasdougehrcly) — socios, anticipos, extras, saldos, chat, conexioneslpulmjzboogixbdxxayo) — recaudaciones diarias, divisores, notasLa integración usa un interceptor
window.fetchensupabase-api.jsque redirige transparentemente las llamadas GAS hacia Supabase sin modificarapp.js.
propi.solicitada/
├── index.html # HTML puro (878 líneas) — estructura de la app
├── app.css # CSS + configuración de estilos (270 líneas)
├── app.js # JavaScript completo de la app (2435 líneas)
├── supabase-api.js # Interceptor fetch → Supabase (nuevo)
├── originalindex.html # Copia completa en un solo archivo (auto-generada)
├── build.sh # Script que reconstruye originalindex.html
├── sw.js # Service Worker — caché v12, network-first
├── manifest.json # Manifiesto PWA
├── vercel.json # Cabeceras HTTP para despliegue en Vercel
├── gas/
│ └── code.gs # Google Apps Script completo (referencia histórica)
├── migration/
│ └── migrate_to_supabase.gs # Instrucciones de migración (referencia)
├── img/
│ ├── icon-192x192.png
│ └── icon-512x512.png
└── .claude/
└── settings.json # Hooks Claude Code (auto-rebuild originalindex.html)
El archivo de trabajo principal es index.html + app.css + app.js.
originalindex.html se regenera automáticamente con ./build.sh cada vez que se edita cualquiera de los tres archivos separados (via hook de Claude Code en .claude/settings.json).
| Token | Color | Uso |
|---|---|---|
lm-bg | #f8f9fa | Fondo general de la app |
lm-card | #ffffff | Tarjetas y superficies |
lm-border | #e1e3e4 | Bordes |
lm-primary | #001723 | Color principal (azul marino) |
lm-primary-mid | #002d40 | Hover de primario |
lm-accent | #264b5f | Acento secundario |
lm-secondary | #006a62 | Verde (éxito / ganancias) |
lm-red | #ba1a1a | Rojo (descuentos / anticipos) |
lm-muted | #72787d | Texto secundario |
lm-subtle | #f3f4f5 | Fondos sutiles |
wa-bg | #e5ddd5 | Fondo chat WhatsApp-style |
wa-mine | #d9fdd3 | Burbuja mensaje propio |
| ID | Ícono | Descripción |
|---|---|---|
tab-home | account_balance_wallet | Balance — monto a cobrar, remanente, valor punto por noche, resumen contable, últimos movimientos, calendario |
tab-history | receipt_long | Historial — dos sub-vistas: Rendimientos (detalle diario) y Anticipos Anteriores (histórico con filtros) |
tab-stats | bar_chart | Estadísticas — gráficos del período activo |
tab-chat | chat | Mensajes — chat Soporte (admin) y Equipo (entre socios) |
tab-perfil | badge | Perfil — datos del socio, años, puntos de antigüedad |
<header class="px-5 pt-safe-top py-3.5 flex items-center justify-between shrink-0 glass z-20">
pt-safe-top: clase personalizada = calc(env(safe-area-inset-top) + 0.875rem) → respeta notch/Dynamic Island en iPhoneglass: fondo semi-transparente con backdrop-filter: blur(24px)#001723 → #002d40 → #264b5f#remateTag): sobrante del período anterior#globalPtsTag): valor unitario del punto en el período<nav id="bottomNav" class="fixed bottom-0 w-full z-[60] pb-safe-bottom">
pb-safe-bottom: clase personalizada = env(safe-area-inset-bottom) → respeta home indicator iPhonetop: calc(env(safe-area-inset-top) + 16px) → aparece debajo del notchcubic-bezier(0.34, 1.56, 0.64, 1)Toggle entre dos vistas: Rendimientos | Anticipos Anteriores
La vista de anticipos incluye:
Funciones JS relacionadas:
switchHistView(view) // alterna Rendimientos / Anticipos Anteriores
loadHistorialAnticipos() // fetch con lazy-load (solo carga una vez)
buildFiltroAnios(data) // construye pastillas de año
buildFiltroMeses(anio, data) // construye pastillas de mes según año activo
filtrarAnticipos(anio) // filtra por año + resetea filtro de mes
filtrarAnticiposMes(mes) // filtra por mes sobre los datos del año activo
togglePeriodo(id) // expande/colapsa un acordeón de período
renderHistorialAnticipos(data) // renderiza la lista de acordeones
_extraerAnio(periodo) // extrae año con regex /\b(20\d{2})\b/
_extraerMes(periodo) // extrae mes de nombres como "Cierre 2025-03" o "Marzo 2025"
helpSlides con 11 slides (actualizado en cada feature relevante){ icon, color, title, body, preview } donde preview es HTML con mockup visual? en el headerEl GAS completo de referencia está en gas/code.gs. Los cambios exclusivos de esta app se describen abajo.
SCRIPT_URL_SOCIOS (doGet + doPost)
| Action | Método | Descripción |
|---|---|---|
getSocios | GET | Lista todos los socios |
getDatosSocio | GET | Datos de un socio específico |
getAllDataDesdeSheets | POST | Datos completos de anticipos, extras, notas del socio |
getSaldosAnteriores | GET | Saldos anteriores del período |
getSaldosCierre | GET | Saldos de cierre / remanentes |
getDiasPartTime | GET | Días trabajados (socios Part-Time) |
getNotes | GET | Notas del período (mensajes admin) |
pingConexion | POST | Heartbeat cada 2 min — deja constancia de la conexión en Supabase |
logoutConexion | POST | Registra el cierre de sesión y borra UltimaConexion |
getHistorialConexiones | GET | Historial de conexiones del socio |
addNote / deleteNote | POST | CRUD de notas admin |
registrarBatchAnticipos | POST | Registra múltiples anticipos |
registrarBatchExtras | POST | Registra movimientos extra |
actualizarAnticipo | POST | Modifica un anticipo existente |
reiniciarAnticipos / reiniciarExtras | POST | Borra anticipos/extras del período |
borrarMovimiento | POST | Elimina un movimiento por UUID |
registrarSaldoAnterior | POST | Guarda saldo anterior para el próximo período |
guardarDiasPartTime / guardarBatchDiasPartTime | POST | Guarda días Part-Time |
guardarCierreIndividual | POST | Guarda cierre de un socio |
SCRIPT_URL_RECAUDACIONES (doGet)
| Action | Descripción |
|---|---|
get | Datos de recaudación diaria del período activo |
getNotes | Notas de recaudaciones |
ping | Verificación de conexión |
| Constante GAS | Nombre de hoja | Columnas principales |
|---|---|---|
HOJA_SOCIOS | Socios | ID, Nombre, Apellido, FechaIngreso, Area, TipoContrato, UltimaConexion |
HOJA_ANTICIPOS | Anticipos | ID Socio, Nombre Completo, Fecha Anticipo, Monto, Estado, UUID, Responsable, AreaResponsable |
HOJA_EXTRAS | MovimientosExtras | ID Socio, Nombre, Fecha, Tipo, Monto, Detalle/Nota, Estado, UUID |
HOJA_SALDOS | SaldosAnteriores | ID Socio, Nombre, MontoSaldo, UltimaActualizacion |
HOJA_DIAS_PT | DiasPartTime | ID Socio, Nombre, DiasJSON |
HOJA_SALDOS_CIERRE | SaldosCierreMes | ID Socio, Nombre, MontoRemanente, FechaCierre |
HOJA_CHAT_SOCIAL | MensajesApp | Mensajería entre socios |
HOJA_ANTICIPOS_HISTORIAL | AnticiposGuardados | Anticipos de períodos cerrados (cols: ID, ?, Fecha, Monto, FechaCierre, ?, Responsable) |
HOJA_HISTORIAL_CONEXIONES | HistorialConexiones | Log de conexiones por socio |
HOJA_AUDITORIA | AuditoriaLogs | Auditoría de todas las acciones |
HOJA_CREDENCIALES | Credenciales | PINs y credenciales de socios |
HOJA_RETIROS_ANTICIPOS | RetirosAnticipos | Retiros de anticipos registrados |
HOJA_MATERIALES | RecaudacionMateriales | Materiales de recaudación |
| — | Anticipos_* | Hojas dinámicas por período (ej: Anticipos_2025-Enero) para historial completo |
Archivo: sw.js — versión boveda-personal-v14
Estrategia:
index.html, app.css, app.js, manifest.json, imágenes) → Network-first (siempre intenta red, fallback a caché)script.google.com (GAS) → Always network, nunca caché, fallback a JSON de errorActualizaciones:
skipWaiting() en install → el nuevo SW se activa de inmediatoclients.claim() en activate → toma control de todas las pestañas abiertasshowUpdateBanner() en el front; el usuario acepta y el SW envía SKIP_WAITINGvisibilitychange (cada vez que la app vuelve al frente)Archivos cacheados:
index.html · app.css · app.js · manifest.json
img/icon-192x192.png · img/icon-512x512.png
Headers especiales (vercel.json):
sw.js → Cache-Control: no-cache, no-store, must-revalidate (el service worker nunca se sirve desde caché del servidor)Service-Worker-Allowed: /La app se despliega en Vercel directamente desde este repositorio GitHub (33Javier33/propi.solicitada), rama main.
Rama principal: main
Rama de desarrollo Claude Code: claude/sheets-to-supabase-migration-i1dlcm (migración Supabase)
Para actualizar la app en producción: hacer push a main. Vercel detecta el push y redistribuye automáticamente.
Reinstalación del PWA en iPhone (tras cambios de CSS/JS): El service worker usa network-first, por lo que los cambios se aplican en la siguiente apertura con conexión. Si el SW no se actualiza, desinstalar la app del home screen y volver a agregarla.
| Tarea | Archivo |
|---|---|
| Estructura HTML, modales, layout | index.html |
| Estilos, CSS variables, clases custom | app.css |
| Lógica JS, funciones, llamadas GAS | app.js |
| Adaptador Supabase / interceptor fetch | supabase-api.js |
| Backend / lógica Google Sheets (referencia) | gas/code.gs |
| Caché PWA | sw.js (incrementar CACHE_NAME al cambiar archivos) |
Al editar index.html, app.css o app.js dentro de Claude Code, el hook en .claude/settings.json ejecuta automáticamente ./build.sh, que regenera originalindex.html (versión completa en un solo archivo).
Para ejecutar manualmente:
./build.sh
Cada vez que se publican cambios en archivos del front-end, incrementar el número de versión en sw.js:
const CACHE_NAME = 'boveda-personal-v14'; // ← incrementar
git add -p # revisar cambios antes de agregar
git commit -m "descripción"
git push -u origin main
Importante: Cada vez que se implementa un cambio relevante en la app, actualizar la sección Historial de Cambios en este README.
El "hoy" del campo de fecha se calculaba con toISOString(), que da la fecha en UTC — tres o cuatro horas adelante de Chile. A partir de las 20:00 o 21:00 el modal se abría con el día siguiente, y en un casino que trabaja de noche eso es casi todos los turnos.
Medido con el reloj puesto a las 22:30 del 3 de septiembre en horario de Chile: la forma vieja decía 2026-09-04; la nueva dice 2026-09-03.
Además, la fecha se queda donde la dejen mientras la app esté abierta: si se corrige una vez, el siguiente reporte se abre en esa misma fecha en vez de volver a hoy. Abrir el modal tocando un día faltante sigue mandando sobre todo lo demás — esa fecha es explícita y gana.
El texto decía que desde el 15 de noviembre de 2025 «cumple el rol de Encargado del Fondo Solidario de Propina», sin término. El cargo va hasta el 15 de noviembre de 2026; después continúa como socio del fondo y a cargo de esta plataforma.
Redactado para que sea cierto hoy y después, sin tener que volver a tocarlo cuando llegue la fecha:
Colaborador del Casino de Puerto Varas desde 2013, actualmente ejerce como Croupier. Encargado del Fondo Solidario de Propina desde el 15 de noviembre de 2025 hasta el 15 de noviembre de 2026, velando por los derechos económicos de todos los socios. Seguirá como socio del fondo y a cargo del desarrollo y la mantención de esta plataforma.
También: el nombre pasa de «Carlos» a «Carlos P. Nauto», y se retira la línea «Encargado Fondo Solidario · Desde 15/11/2025» que iba bajo él — decía lo mismo sin el término y quedaba contradiciendo al párrafo.
Comparando el comprobante de esta app con el recibo de socios-comicion apareció que los dos decían mal esa línea, de formas distintas. El grueso del error estaba en la administración (usaba el mes de la primera entrega en vez del del aniversario), pero acá había dos detalles:
Nuevo _subePuntosLabel(), que aplica la misma regla que reglaPuntosFechas en socios-comicion: día 15 del mes de ingreso, o del mes del override si lo hay. Las tres apps dicen ahora lo mismo, verificado corriendo sus implementaciones reales sobre los mismos socios.
La pantalla de ingreso mostraba el ID completo: ID: SOC-1764786246858. Ese dato se ve antes de autenticarse —cualquiera que tome el teléfono lo lee— y junto con el PIN es la credencial de entrada. Ahora va tapado: ID: SOC-************.
Se conserva el prefijo (SOC-) para que el socio reconozca que es el suyo, y el resto en asteriscos. La cantidad es fija a propósito: calcarla al largo real delataría cuántos dígitos tiene.
Dentro de la app, ya autenticado, el ID se sigue viendo en el perfil — el socio necesita poder leerlo para dárselo a la administración.
8 comprobaciones más en test_saldo_oculto.js (50 en total): que no quede ningún dígito, que dos IDs de largo distinto se tapen igual, que un ID sin prefijo también se tape, y que sin ID no se invente nada.
Dos cosas que quedaron pendientes del candado:
1. El número mostraba un recorte del ID, no el ID. Se le quitaban las letras y se tomaban los últimos cuatro dígitos: un ID como SOC-001 quedaba en 001, que no se reconoce como el de nadie. Ahora va el ID completo con el que el socio entra, en el último grupo: •••• •••• •••• SOC-001. Para un ID numérico corto el resultado se ve igual que antes, pero ahora es el dato real y no una coincidencia.
2. Con el candado puesto el número seguía legible. Quedaba el balance tapado y el ID del socio a la vista en la misma tarjeta — justo lo que el candado busca evitar. Ahora se tapa también, con su propia máscara (•••• •••• •••• ••••) y no con la del monto: _ponerMonto() acepta una máscara por elemento, así que cada cosa se tapa con lo que le corresponde.
test_saldo_oculto.js sube a 42 comprobaciones: que el número sea el ID, que un ID con letras se muestre entero, que el candado lo tape sin dejar ver los dígitos, que use su máscara y no la del monto, y que al destapar vuelva.
Como en las apps del banco: un ojito en la tarjeta tapa el balance para que no quede a la vista de cualquiera que mire la pantalla, y lo vuelve a mostrar. La elección se guarda, así que al cerrar y volver a abrir la app sigue como la dejaste.
Con el candado puesto se tapan todos los montos personales —el balance de la tarjeta, el de arriba, el remanente y los del panel premium— con $ ••••••. No se tapan los puntos ni el valor punto por noche: no son plata de nadie en particular.
Se tapa al escribir, no después. Si el monto real se pintara y se ocultara en un segundo paso, alcanzaría a verse en cada refresco. _ponerMonto() guarda el valor real en data-real y pinta la máscara o el monto según el candado, así que el número nunca pasa por la pantalla. La prueba lo vigila con un MutationObserver sobre el nodo durante un refresco completo: el monto real no aparece ni una vez.
Dos detalles que podrían haber salido mal:
•••• pegado. Ahora leen el valor real, no lo que está en pantalla.test_saldo_oculto.js: 31 comprobaciones — tapar, destapar, que persista tras recargar la app entera, que el refresco no filtre el monto, y que el comprobante siga completo.
La causa, en una línea: el shim mapeaba fecha_inicio_puntos al nombre FechaInicioLiquidacion, que no lo leía nadie, mientras el cálculo buscaba FechaInicioPuntos, que nadie escribía. El campo llegaba a la app y se perdía en silencio.
Un campo escrito y nunca leído, y otro leído y nunca escrito. Sin error en consola, sin dato vacío a la vista: simplemente el override de la fecha de inicio de puntos no se aplicaba, así que a los socios con fecha movida a mano se les contaban puntos desde la fecha equivocada —o se les contaban cuando todavía no correspondía— y los dos números no cuadraban con los de la administración.
Se nota ahora porque hace dos versiones se arregló que esa fecha se pudiera guardar en socios-comicion (antes el volcado de la planilla la pisaba). Recién cuando empezó a haber overrides reales quedó a la vista que esta app los ignoraba.
El arreglo. getSocios entrega FechaInicioPuntos, el nombre que usan socios-comicion y diario.propi. Se conserva FechaInicioLiquidacion como alias, y el lado que lee acepta los dos: los socios guardados en la caché local por versiones anteriores solo traen el nombre viejo, y sin el alias los puntos seguirían saliendo mal hasta que la caché se renovara.
De paso, los años de antigüedad del propio socio salían del ingreso aunque tuviera override. Nuevo _aniosDeSocio() los resuelve en un solo lugar, igual que procesarSocioDesdeGoogle en la administración.
Verificación. test_puntos_propi.js levanta las dos apps y corre sus implementaciones reales —procesarSocioDesdeGoogle y _recCalcTotalPuntos— sobre los mismos socios, exigiendo que coincidan. Los casos que separan a las dos: override hacia atrás, override a futuro (aún no le corren puntos), override igual al ingreso (es un eco, no una excepción), recién ingresado sin cumplir el mes, y Gastos Comisión. 21 comprobaciones, incluida la de la caché vieja.
Una diferencia que se deja a propósito: en Gastos Comisión la administración registra 0 años porque la antigüedad no les mueve los puntos (siempre 1), pero la app del socio le muestra su antigüedad real. Son dos cosas distintas y las dos están bien; la prueba lo fija explícitamente para que nadie lo «corrija» después.
El problema. Cuando la administración le cerraba el mes a un socio y éste retiraba su plata, la app le seguía mostrando el monto completo, como si aún tuviera que recibirlo. La recaudación del período sigue cargada —no se archiva— así que el balance se volvía a calcular entero.
La regla, la misma que ahora aplica socios-comicion:
| Estado del cierre | Qué ve el socio |
|---|---|
| Cobrado | Balance en $0 — parte el mes nuevo solo con su saldo anterior (el remanente del cierre) |
| En sobre | El monto se mantiene: la plata es suya y lo está esperando. Solo se le avisa que el mes ya se cerró y que pase a retirarlo |
Esa distinción importa: poner en $0 al socio que todavía no retira le escondería plata que es suya.
Cómo lo sabe. Nueva acción getCierresMes en supabase-api.js, que lee la tabla cierres_mes del proyecto de socios — la misma que escribe la administración al cerrar. Mientras la fila exista, ese mes está liquidado. Se vacía al archivar el período, que es cuando empieza el mes siguiente, así que el socio vuelve a acumular solo. No hizo falta ninguna tabla nueva.
El aviso. Sobre la tarjeta de balance, para que ver $0 no parezca que la app perdió sus datos:
✅ Tu mes ya fue pagado — Se cerró el 14 de octubre y ya retiraste $95.000. Por eso tu balance parte en $0: lo que ves abajo es tu saldo para el mes nuevo.
El comprobante sigue sirviendo. Antes se bloqueaba con «los datos aún no han cargado» justo cuando el balance quedaba en 0 — o sea, exactamente después de cobrar. Ahora, si hay cierre, imprime lo que se liquidó (mismo criterio que el recibo del admin), no los ceros del mes nuevo.
Si no se puede leer el cierre, el monto no reaparece. Una lectura caída conserva el último cierre conocido. Sin esto, un corte de red le devolvería al socio un monto que ya cobró — que es justo el error que se está corrigiendo.
test_propi_cierre.js: 25 comprobaciones sobre los tres estados (sin cierre, en sobre, cobrado), el comprobante, que la recaudación quede intacta, y el caso de la lectura caída.
La app ya sabía mostrar mensajes con formato: notaHTML() los pinta saneados con la misma lista blanca que usa socios-comicion en sus Notas. Lo que faltaba era poder escribirlos — el cuadro mandaba innerText y el formato se perdía al enviar.
Nueva barra de formato, con los mismos nueve botones que socios-comicion:
| B · I · U | negrita, cursiva, subrayado |
| ⯇ ≡ ⯈ | alinear a la izquierda, centrar, a la derecha |
| •— · 1. | lista con viñetas, lista numerada |
| ✕ | quitar formato |
Se abre con el botón Aa, que va dentro de la burbuja de escritura: un cuarto botón redondo afuera dejaba el campo muy angosto en un teléfono. Queda cerrada por omisión — sobre el teclado hay poco espacio y la mayoría de los mensajes no llevan formato. La barra se desliza en horizontal en vez de apilarse, para no empujar el teclado.
Detalles que importan:
getChatText() devuelve el HTML pasado por sanearNota(), la misma función con la que se pinta. Nada que no sobreviva al saneado llega a viajar.Los tres modos del chat (Administración, Social y Privado) ya pintaban con notaHTML(), así que el formato se ve en los tres. Y como la lista blanca es idéntica a la de socios-comicion (B,STRONG,I,EM,U,BR,P,DIV,SPAN,UL,OL,LI,A), lo que se redacta acá se ve igual allá y al revés.
test_chat_formato.js: 43 comprobaciones, incluidas las de saneado (script, img con onerror, enlaces javascript:, estilos no permitidos).
Dos cambios que van juntos:
1. El balance se muestra siempre como tarjeta. Hasta ahora convivían dos vistas —la clásica (#pmBalanceCard) y la tarjeta— con un botón para saltar de una a otra. Desde que la tarjeta tiene ocho versiones para elegir en Configuración, la clásica sobraba. Se retiró el bloque, se quitaron los botones de cambio de vista y la tarjeta queda limpia, sin nada encima.
2. El comprobante sube al header. El botón de descargar comprobante vivía escondido dentro de la tarjeta. Ahora está arriba, junto a la campana, la ayuda y el botón de salir, con las mismas clases que ellos para que se vea igual.
En su lugar se quitó la píldora con el área (● mesas), porque era el mismo dato dos veces: el área ya sale justo debajo del saludo, en «mesas · Casino Puerto Varas» (#pmRolTxt), que es la que se queda.
Un error que la prueba destapó. Al dejar la tarjeta siempre visible,
_aplicarBalanceEstilopasó a llamar siempre a_refrescarTarjeta, y esa leecurrentUser.ID. Antes solo corría al elegir la vista «tarjeta», así que nunca se topaba concurrentUseren nulo; ahora corre también al cargar la app antes de entrar, y reventaba en el arranque. Lleva su guarda, y la prueba cubre ese caso.
Archivos: index.html, app.js, originalindex.html, sw.js, version.js.
Los dos números que salen al abrir Recaudación del Día no coincidían con el panel de Gestión de Socios de socios-comicion ni con la franja de diario.propi.
La causa: una tercera copia de la regla, con la versión vieja. _recCalcTotalPuntos rehacía por su cuenta el cálculo de cuándo empiezan los puntos, y lo hacía mal:
| Cuándo empieza a contar un socio | |
|---|---|
| socios-comicion y diario.propi | El primer día 15 en o después de cumplir el primer mes |
| Recaudación del Día (lo que hacía) | El día 15 del mes de ingreso |
Con el Ejemplo A de la política —ingresa el 4 de septiembre— este botón le contaba puntos desde el 15 de septiembre y las otras dos apps desde el 15 de octubre. Sobre la nómina de prueba daba 123 / 83 donde las otras dos daban 119 / 79.
Lo llamativo es que esta app ya tenía la regla correcta en _aniosPuntos; el agregado simplemente no la usaba. La causa de fondo era que el cálculo de la primera entrega estaba enterrado dentro de _aniosPuntos, así que quien necesitaba ese dato lo rehacía. Ahora está suelto en _primeraEntregaPuntos() y las dos funciones lo comparten.
De paso se corrigió que fecha_inicio_puntos manda solo cuando difiere de la fecha de ingreso: si viene igual no es una excepción manual, es un eco del ingreso.
Después del cambio: las tres apps dan lo mismo en 293.460 combinaciones de fecha de ingreso, área y puntaje guardado.
Se barrieron los tres repos buscando una cuarta copia de la regla. No hay: los únicos
new Date(año, mes, 15)que quedan en el cálculo de puntos están en la rama del override manual, donde sí corresponde. El resto de los «día 15» del código son el cálculo del período 15→14, que es otra cosa.
Archivos: app.js, index.html, originalindex.html, sw.js, version.js.
La tarjeta no seguía ningún tema: daba igual que la app estuviera en claro, oscuro o rosa, siempre era el mismo azul petróleo. Los colores estaban escritos a mano en el HTML, y había incluso una regla en app.css que la forzaba a blanco para que el tema no la tocara.
Ahora se elige entre ocho diseños, todos conservando «Fondo Solidario · Casino Puerto Varas»:
| Diseño | Cómo es |
|---|---|
| Medianoche | La de siempre: azul petróleo con chip dorado. Sigue siendo la que viene puesta |
| Ónix | Negro con chip dorado |
| Océano | Azul profundo con chip celeste |
| Esmeralda | Verde oscuro con chip menta |
| Vino | Burdeos con chip rosado |
| Pasto | Verde oliva con chip lima |
| Lavanda | Violeta con chip lila |
| Platino | Clara, plateada, con texto azul noche y chip dorado viejo |
Se elige en Configuración → Versión de la tarjeta, con una miniatura de cada una. Va aparte del tema de la app, a propósito: el tema viste la app y la tarjeta es la tarjeta, así que se puede tener la app en oscuro y la tarjeta en Platino. Se guarda en ese dispositivo, como el tema.
Cómo está hecho. Todo el color sale de --tj-base, que es el color del texto en RGB suelto; los grises de la tarjeta son ese mismo color con transparencia. Así, una tarjeta clara solo cambia esa variable y toda la jerarquía se da vuelta sola — es lo que hace posible Platino sin duplicar ni una regla.
Las transparencias no son a ojo. Se midieron contra el tope más claro del degradado de cada diseño, que es el caso peor. Con las que tenía la tarjeta original, «Casino Puerto Varas» daba 4,34:1 en Medianoche y habría dado 3,83:1 en Pasto, bajo el mínimo de 4,5. Subidas, el peor de los ocho diseños queda en 5,37:1.
Dos cosas que se arreglaron de paso: la tarjeta original ya venía con ese contraste flojo, y los nombres del selector usaban al principio variables de socios-comicion (
--rf-suave,--rf-bd) que en esta app no existen, así que caían a un gris ilegible sobre fondo oscuro. Ahora llevan su variante explícita paradata-theme="oscuro", que cubre de una vez a Oscuro, Negro y todos los temas con tinte.
Archivos: app.css, app.js, index.html, originalindex.html, sw.js, version.js.
tab-chat), la capa fija del chat tenía un z-index: 40 mientras que el <header> superior de la aplicación tenía un z-index: 20. Esto provocaba que la sección de mensajes cubriera los botones del encabezado (incluido el botón para cerrar sesión logout()), impidiendo cerrar la app o interactuar con el header estando en la pestaña Mensajes. Se elevó la clase del header a z-50, de modo que queda correctamente posicionado sobre el chat (z-40) y debajo de la barra de navegación inferior (z-60).boveda-personal-v186 en sw.js, version.js e index.html.Archivos: index.html, sw.js, version.js, README.md, originalindex.html (regenerado).
src="" vacío en la etiqueta <img id="fotoGrandeImg"> de index.html por un data URI SVG transparente placeholder (data:image/svg+xml,...). Esto evita peticiones HTTP redundantes a la raíz al cargar el documento y resuelve la advertencia/error detectado por las herramientas de validación HTML.boveda-personal-v185 en sw.js, version.js e index.html para asegurar que los navegadores y PWA actualicen sus recursos cacheados.Archivos: index.html, sw.js, version.js, README.md, originalindex.html (regenerado).
En Configuración → Tema de la app había doce fondos. Ahora hay dieciséis:
| Fondo | Cómo es |
|---|---|
| Cielo ☁️ | Azul de día: fondo celeste muy suave, textos en azul marino. |
| Océano 🌊 | Azul de noche, más azul que el Oscuro de fábrica (que tira a gris). |
| Pasto 🌱 | Verde pasto de verdad: tira a amarillo, no a azul como Menta. |
| Campo 🌳 | El Pasto de noche: el mismo verde lima, pero sobre fondo oscuro. |
Funcionan como los que ya estaban: se eligen con un toque, se guardan en ese teléfono, tiñen la barra de arriba del celular y vuelven puestos al abrir la app, sin el parpadeo a blanco.
Cielo y Pasto son temas claros completos; Océano y Campo se montan sobre la base oscura con un tinte (data-tinte), igual que Esmeralda, Grafito, Vino e Índigo.
Un detalle de color: en Pasto el acento es verde oliva #4d7c0f y no el lima #65a30d — sobre el lima, el texto blanco de las pastillas daba 3,1:1 y no se leía; con el oliva da 5,1:1. En Cielo pasa lo mismo: el acento es #2563eb y no #3b82f6.
Archivos: app.css, app.js, index.html, sw.js.
Acá los años se contaban por el día exacto de ingreso: quien entró un 23 de octubre sumaba un año el 23 de octubre. En la app de la comisión el aumento cae el 15 del mes de ingreso. Durante esas dos semanas el socio veía un número distinto al que tenía la comisión.
Ahora el cálculo es el mismo en las dos apps, siguiendo la política: los primeros puntos llegan el primer día 15 al cumplir un mes de contrato, y suben cada 15 del mes de ingreso.
Afecta lo que se muestra cuando la base no trae un valor guardado de puntos; si el socio ya tiene puntos cargados, ese número sigue mandando, igual que antes.
Verificación: las mismas 32 comprobaciones compartidas con socios-comicion, más el perfil del socio mostrando los puntos que corresponden.
Archivos: app.js, index.html, sw.js, version.js, originalindex.html (regenerado).
La regla: todos parten en 4 puntos y suman +2 por año cumplido hasta el tope de su área. Bóveda es la única excepción: parte en 2, tope 10.
La fórmula estaba escrita tres veces en app.js y dos de ellas no tenían la regla de Bóveda: al socio de bóveda su propio perfil le calculaba el base 4. Con 2 años de antigüedad le mostraba 8 puntos donde le corresponden 6.
Además, el área se comparaba con tildes en uno de esos lugares ('bóveda' !== 'boveda'), lo que también dejaba fuera a Máquinas y Técnicos de su tope de 12.
Ahora las tres salen de un solo sitio, _reglaPuntosArea, que compara el área sin tildes ni espacios y dice lo mismo que reglaPuntosArea de socios-comicion.
Esto solo afecta lo que se MUESTRA cuando la base no trae un valor guardado. Si el socio tiene puntos cargados en Supabase, ese número sigue mandando, igual que antes — y si ese número está mal, se corrige desde la app de la comisión, que ahora avisa de los casos así.
Verificación: 32 comprobaciones compartidas con socios-comicion — base y tope de cada área escrita con tilde, sin tilde, en mayúsculas y con espacios; la serie año por año; que Bóveda sea la única que parte en 2; y que el perfil del socio de bóveda con 2 años muestre 6 y no 8.
Archivos: app.js, index.html, sw.js, version.js, originalindex.html (regenerado).
Si el socio faltó varios días seguidos —una licencia, por ejemplo—, en Ver Calendario → Ausencias del período aparecían como una tarjeta suelta por día. Con una licencia de 19 días había que sumar de cabeza para saber cuánto se dejó de percibir.
Ahora los días seguidos del mismo motivo son UNA tarjeta: Del 16 de septiembre al 20 de septiembre · Licencia Médica · 5 días · −$100.000. Tocándola se abre y muestra día por día cuánto fue cada uno, con el total al final.
Mismo criterio que en la app de la comisión: seguidos y del mismo motivo. Una licencia pegada a un permiso son dos tramos, no uno; y una ausencia de un día solo se sigue viendo como una tarjeta normal, sin flecha.
El motivo ahora viaja con el día, así la tarjeta puede decir de qué fue la ausencia — antes solo se veía la fecha y el monto.
Los totales de arriba no cambiaron: siguen contando los días reales del período (8 días, no 3 tarjetas) y el monto completo.
El Part-Time queda exactamente igual: sus días trabajados siguen uno por tarjeta, en verde y sumando.
Verificación: 19 comprobaciones — que 8 días queden en 3 tarjetas; que el permiso pegado vaya aparte; que el día suelto no se pliegue; abrir, cerrar y la flecha; que el detalle liste sus 5 días con el total al final; que los contadores de arriba sigan con los días reales; el Part-Time intacto; y la lista vacía sin romperse.
Nota honesta sobre el contraste: en este entorno no se puede medir, porque las clases de color de la app (text-lm-*, bg-lm-*) vienen del CDN de Tailwind, que está bloqueado. Lo que sí se verificó es que el bloque nuevo no trae ningún color escrito a mano: usa las mismas clases que las tarjetas que ya estaban en esa lista.
Archivos: app.js, index.html, sw.js, version.js, originalindex.html (regenerado).
La guía de bienvenida pasa de 23 a 24 diapositivas.
Nueva: «Entrar con el QR de la administración». Explica escanear con la cámara, que pregunta a qué app entrar —esta o el Diario—, que el mismo código sirve una vez en cada una, y que los datos llegan puestos y solo hay que crear el PIN. Con la advertencia clara: quien tenga ese código puede entrar como tú, no lo reenvíes, y vence a los 3 días.
Actualizada: «¿Por qué me pidieron el RUT?». Desde v179 el RUT se compara con el que la administración tenga registrada. La diapositiva decía solo para qué sirve; ahora avisa que tiene que ser el tuyo, explica que eso es lo que impide que otra persona vincule tu cuenta sabiendo solo tu ID, y dice qué hacer si no coincide: pedir el QR de acceso.
Verificación: 12 comprobaciones — que la guía abra y el contador diga 24, que la diapositiva del QR se pueda abrir y su texto se pinte, y que los avisos de seguridad estén en el texto.
Archivos: app.js, sw.js, version.js, originalindex.html (regenerado).
El QR que se genera en socios-comicion abre esta app con los datos del socio ya puestos. Al escanearlo pregunta a qué app quiere entrar —esta o diario.propi— y sigue desde ahí.
?qr=<32 hex>) que se canjea contra Supabase. Así caduca, se puede anular, y ni el nombre ni el RUT viajan en la URL.Un problema que apareció al probarlo, y que habría roto justo el caso que importa: en la primera visita el Service Worker se instala, toma el control y la página se recarga sola (index.html, controllerchange). El código ya había salido de la URL, así que esa recarga se lo llevaba — y la primera visita es exactamente la del socio que estrena la app con su QR. Ahora el código se guarda en sessionStorage en cuanto se lee y de ahí se recupera tras la recarga; se olvida en cuanto se canjea, para no reintentar un código ya usado. sessionStorage es el lugar correcto: sobrevive la recarga y muere al cerrar la pestaña.
Además, la vinculación ahora compara el RUT. Antes handleSetup buscaba al socio solo por ID y del RUT validaba únicamente el formato, nunca lo comparaba con el guardado (app.js 241 y 247). Como los IDs son adivinables (SOC-…), con un ID válido cualquiera podía vincularse como ese socio y crearle el PIN. Ahora, si el socio tiene RUT en su ficha, tiene que coincidir. Si no lo tiene no hay contra qué comparar, así que se acepta y queda registrado — es el caso de la mayoría, y el QR es el camino para ir completándolos.
Verificación: 19 comprobaciones del escaneo (elección de app, que el código salga de la URL antes de canjear nada, socio sin datos vs. socio completo, correo inválido rechazado, que lo completado se mande con el código, código vencido) y 6 del RUT: con un RUT ajeno no vincula y lo dice; con el suyo sí; y si su ficha no tiene RUT, se acepta el que escriba.
Archivos: qr-entrada.js (nuevo), app.js, index.html, sw.js, version.js, vercel.json, originalindex.html (regenerado).
type="password" ante cualquier cambio de la página y, como ya tenía una credencial guardada para el sitio, salía a ofrecer actualizarla una y otra vez. El autocomplete="off" que ya tenía no sirve para esto: los gestores lo ignoran para el aviso de guardar.type="password"; al cerrar sesión vuelve a serlo, que es cuando de verdad se usa. Mientras usas la app no queda ningún campo de contraseña en la página, así que el navegador no tiene nada que ofrecer..campo-secreto). Se ven igual de ocultos, pero el navegador no los confunde con la clave del sitio. El único type="password" que queda es el del login, que es el que corresponde.revisar-despliegue.py, que comprueba que vercel.json solo use claves que Vercel acepta y que todo archivo local que carga el HTML tenga no-caché o ?v=.version.js no estaba en la lista de no-caché de vercel.json y además se publicó dos veces con la misma dirección (?v=1): el navegador se quedó con la copia del primer día —la que tenía el error de mostrar la versión en espera— y nunca volvió a pedir el archivo. Por eso el número no cambiaba.version.js a las reglas de no-caché (la cabecera sola no alcanza, porque no se aplica a una copia que el navegador ya considera fresca) y se cambió la dirección a ?v=3, que es lo que la obliga a pedirla de nuevo.manifest2.json, index2.html y sw2.js en socios-comicion; manifest.json, version.js, /icons/ e /img/ en propi.solicitada. Resultado ahora: 53 recursos revisados en las cuatro apps, 0 sin protección.version.js, index.html, app.css, sw.js.app-version, así que sumar el dato en otro lugar es poner un <span class="app-version">.version.js (nuevo), index.html, sw.js.pingConexion y logoutConexion ya escribían en Supabase, pero además reenviaban la llamada al GAS (_origFetch / _origBeacon) solo para que el script mandara el aviso al chat. Eso significaba que cada conexión, cada latido cada 2 minutos y cada cierre de sesión de cada socio salían del navegador hacia un servidor externo. Ese reenvío se eliminó.pingConexion + logoutConexion + el sendBeacon de cierre producían 3 salidas a la red; ahora producen 0, y el registro en Supabase se mantiene igual.gas/code.gs se quitó el token, el chat_id, las funciones de envío, el webhook de entrada (doTelegramWebhook y su despacho en doPost), los 8 comandos de consulta y los 9 avisos automáticos.logActividad() en la tabla conexiones_log de Supabase, que nunca pasó por Telegram — incluida la desconexión.supabase-api.js, app.js, gas/code.gs, README.md.⚠️ Pendiente fuera del repositorio: el token del bot estaba escrito en
gas/code.gsy quedó en el historial de git. Hay que revocarlo en @BotFather — borrar el archivo no lo invalida. Ver el detalle en el README dediario.propi.
cpn-marca.png es casi cuadrado (520×480): el ancho se paga casi entero en alto. En la ficha «Acerca de» medía 250 px de ancho por 231 px de alto — el logo era casi toda la ficha.opacity: .85 para que se lea como firma y no compita con el formulario..marca-logo-panel, .marca-logo-foot) en app.css, con los mismos valores que las otras dos apps, para que el tamaño no se vuelva a desincronizar.app.css, index.html. No cambia el archivo del logo ni sus colores — solo el tamaño.autor: currentUser.Nombre — el nombre de pila. Ahora va el nombre completo.socId, pero addNote no lo guardaba en notas_recaudacion. Ahora sí (la columna se agregó en la base, nueva y opcional).getNotes devolvía socId: null fijo, así que el avatar nunca encontraba al socio. Corregido.app.js (_nombreCompleto, envío de la nota), supabase-api.js (addNote, getNotes). originalindex.html regenerado. SW boveda-personal-v172, versión visible 172.socId del mensaje.avatarSocioHTML): foto si el socio la subió, inicial de color si no. Antes cada chat lo resolvía por su cuenta y se veían distintos. Solo 3 de 67 socios tienen foto, así que la inicial sigue siendo el caso normal y se mantiene tal cual.app.js (avatarSocioHTML, fotoDeSocio, _aplicarFotoPerfil), app.css (.wa-autor-yo, .nav-foto), index.html (botón Perfil). originalindex.html regenerado. SW boveda-personal-v171, versión visible 171.b, strong, i, em, u, br, p, div, span, ul, ol, li, a y los estilos text-align, font-weight, font-style, text-decoration. Los enlaces solo http/https, y con rel="noopener noreferrer". Todo lo demás se convierte en texto. Probado con <script>, onclick, enlaces javascript:, <iframe>, <img onerror> y estilos no permitidos: ninguno sobrevive.linkify como hasta ahora.app.js (notaTraeFormato, sanearNota, notaHTML), app.css (.nota-cuerpo). originalindex.html regenerado. SW boveda-personal-v170, versión visible 170.filter: brightness(1.45) saturate(1.05).img/marca/cpn-marca.png (520×480, 219 KB, fondo transparente). Se borran los SVG del intento anterior.originalindex.html regenerado con build.sh. SW boveda-personal-v169, versión visible 169.index.html, app.css, sw.js, img/marca/, originalindex.html.img/carlospn-logo.png) por el nuevo y «CarlosPN Interactive» pasa a «Carlos P. Nauto Interactive» en sus 5 menciones: el modal Acerca de, el pie del login, la línea de derechos reservados, el crédito de la barra inferior y el pie de los comprobantes que se imprimen (app.js).?v=2026..marca-logo —mix-blend-mode: multiply en claro, invert(1) hue-rotate(180deg) + screen en oscuro— funciona igual con un PNG transparente: no hubo que tocar app.css.originalindex.html regenerado con build.sh. SW boveda-personal-v165, versión visible 165.index.html, app.js, sw.js, img/carlospn-logo.png, originalindex.html.app.js (_notaFaltantesHTML). SW v164 (visible v164).refresh(), después de pintar el balance y la lista de movimientos. Todo lo que va antes se ve; si algo falla en ese tramo, la función se corta ahí y el aviso nunca se pinta, sin ningún error visible en pantalla. Encaja exactamente con lo que se veía._pintarFaltantesHome() y se llama apenas llegan los datos, antes del balance y de los movimientos. Va dentro de un try/catch, así que ni siquiera un error propio lo impide.refresh() sigue con el historial y el resto de la pantalla, en vez de cortarse a la mitad.app.js (_pintarFaltantesHome, refresh). SW v163 (visible v163).app.js (_calcularDiasFaltantes). SW v162 (visible v162).app.js (_calcularDiasFaltantes, _notaFaltantesHTML). SW v161 (visible v161).DONACION_ENTREGA._esDonacion() aceptaba cualquier tipo que contuviera donacion, así que habría tomado también el nuevo DONACION_ENTREGA y descontado del balance el monto completo del retiro. Ahora exige que contenga donacion y no extern ni entrega, igual que en socios-comicion.socio_id ficticio, así que ningún socio lo lee; el ajuste es la segunda barrera para que nunca llegue al balance de nadie.app.js (_esDonacion). SW v160 (visible v160)._esDonacion() detectaba cualquier tipo que contuviera donacion, así que habría tomado también el tipo nuevo DONACION_EXTERNA y descontado un aporte que no le corresponde a ningún socio. Ahora exige que el tipo contenga donacion y no extern, igual que en socios-comicion.socio_id ficticio, así que ningún socio los lee; el ajuste es una segunda barrera para que un error de tipeo en el tipo no llegue nunca al balance de nadie.app.js (_esDonacion). SW v159 (visible v159).truncate / text-overflow: ellipsis, así que un motivo largo se cortaba con puntos suspensivos y el socio no alcanzaba a leer a qué correspondía el descuento. Ahora, solo en las filas de donación, el texto hace salto de línea y se muestra completo.align-items: flex-start) cuando el motivo ocupa varias líneas, para que el icono y el monto no queden flotando al medio.app.js (render de movimientos). SW v158 (visible v158).Acompaña a la sección 💝 Donaciones de socios-comicion (v70): cuando un socio necesita ayuda, la administración abre una colecta y registra cuánto aporta cada socio.
saldoBruto = (propina + saldo anterior) − (anticipos + descuentos + donaciones)._esDonacion), igual que en socios-comicion, para que ninguna variante del tipo (DONACION, Donación, donacion) quede fuera en una app y dentro en la otra.app.js (_esDonacion, _motivoDonacion, saldoBruto, lista de movimientos). SW v157 (visible v157).Acompaña al cambio de socios-comicion (v69), que ahora permite enviar Word, Excel y cualquier archivo al socio. Sin esto, un Excel enviado por la administración aparecía en la app del socio con icono de imagen y sin indicar de qué se trataba.
DOC_MIME_POR_EXT, _docMime): los navegadores dejan file.type vacío en los formatos de Office con macros (.xlsm, .docm, .pptm) y el archivo se subía como binario genérico.index.html (selector y texto), app.js (DOC_MIME_POR_EXT, _docMime, _docTipo, _docTamano, renderDocumentos, subirDocumento). SW v156 (visible v156).Al revisar el borrado de ausencias en socios-comicion se compararon los dos cálculos y aparecieron dos diferencias que hacían que el mismo socio, con los mismos datos, viera un saldo distinto en cada app. Se unificó el criterio con el de socios-comicion (js/anticipos.js).
Set de fechas y descuenta una sola vez. Ahora propi también.AUSENCIA; socios-comicion acepta cualquier tipo que contenga "ausencia" (sin distinguir mayúsculas). Un registro como Ausencia Justificada descontaba en la administración pero no en la app del socio, que mostraba un saldo más alto. Ahora las dos usan la misma regla._ausenciasFechas(extras) y lo usan tanto el cálculo del saldo como el historial y el calendario, para que no vuelvan a separarse.app.js (_ausenciasFechas, cálculo de puntoGlobalTotal, ausenciasSet). SW v155 (visible v155).index.html (etiquetas), app.js (enviarEgreso, aviso de _egrTotalBilletes). SW v154 (visible v154).rpc_responsables_lista() leía únicamente la tabla responsable_creds, que contiene solo a los responsables con PIN personal configurado. La lista real y completa la administra socios-comicion y vive en config_sistema bajo la clave responsables (JSON [{ini, area}]), con los tres: N.M (S.J), P.M (S.J), C.P (S.J)._cargarEncargados() ahora consulta las dos fuentes en paralelo y las fusiona eliminando duplicados por ini|area, ordenadas alfabéticamente. Si una de las dos falla, se usa la otra; nunca se pide el PIN.sql/rpc_responsables_lista.sql en el repo de socios-comicion con la RPC corregida — hace la misma fusión dentro de la base y es SECURITY DEFINER, para que la app no necesite leer config_sistema cuando la Fase 1b cierre el SELECT anónimo sobre esa tabla.app.js (_cargarEncargados). SW v153 (visible v153).3 en $20.000 → $60.000). Las filas en cero quedan con un guion atenuado.rgba, así el bloque se lee bien en los 12 temas.index.html (contenedor), app.js (EGR_GRUPOS, _egrRenderBilletes, _egrTotalBilletes). SW v152 (visible v152).Total desglose: $X. Si el total coincide con el monto solicitado se marca en verde (coincide ✓); si no coincide, en ámbar (no coincide con el monto). Es solo un aviso, no bloquea el envío.billetes (jsonb) de solicitudes_egreso; si esa columna aún no existe en la base, el desglose se anexa a la nota (Desglose: 3×$20.000, 1×$10.000) junto con el encargado, para no perder el dato.index.html (bloque de billetes), app.js (EGR_DENOMS, _egrRenderBilletes, _egrTotalBilletes, _egrLeerBilletes), supabase-api.js (guardado). SW v151 (visible v151).rpc_responsables_lista() — la RPC que no expone los PIN —, con opción "Otro / no aparece en la lista" que habilita un campo libre. La lista queda en caché para que el modal abra al instante.encargado de solicitudes_egreso; si esa columna aún no existe en la base, el dato se anexa a la nota (Encargado: X) para no perderlo.index.html (campos), app.js (_cargarEncargados, envío), supabase-api.js (guardado). SW v150 (visible v150).index.html (#bloquePIN, #btnIngresarPIN, #btnUsarPIN), app.js (_bioRefrescarUI, mostrarIngresoPIN). SW v149 (visible v149).navigator.credentials) con autenticador de plataforma y userVerification: 'required'. La biometría la valida el propio teléfono; la app solo guarda el identificador de la credencial (no la huella)._completarIngreso(auth), usado tanto por el PIN como por la huella. No se almacena el PIN en ninguna parte para habilitar esto.index.html (botón en login + fila en Ajustes), app.js (módulo WebAuthn, _completarIngreso, _bioRefrescarUI). SW v148 (visible v148).logActividadReset) y la ventana bajó a 15s. Además ahora se registra el evento 'desconectado' (cierre de sesión) y se libera la presencia.supabase-api.js, app.js. SW v147 (visible v147).conexiones_log mediante logActividad(evento, nombre, socioId, detalle), con anti-duplicado de 1 minuto por evento para no llenar el registro.supabase-api.js (logActividad), app.js (login + apertura del modal). SW v146 (visible v146).rec_presencia, con clave app:socio). Como las claves eran distintas, el filtro de duplicados no la reconocía y la mostraba dos veces.app + socio_id, o el nombre si no hay id). El aviso de "quién recién entró" usa la misma identidad.supabase-api.js. SW v145 (visible v145).socio_id puede estar en las dos apps (misma cuenta en propi y en diario). El filtro ocultaba cualquier presencia con mi mismo ID, así que borraba justo la línea de "(Diario)" que se quería ver.supabase-api.js. SW v144 (visible v144).socio_id (versión previa), el filtro no la reconocía como propia.app:socio_id), así al recargar se sobreescribe la misma fila y nunca quedan duplicados. Además, red de seguridad para filas antiguas: mismo nombre en la misma app = soy yo. Se limpiaron las filas viejas de la tabla.supabase-api.js. SW v143 (visible v143).socio_id. Si el mismo socio tiene propi.solicitada y diario.propi abiertas a la vez, no se muestra a sí mismo (antes aparecía como si fuera otra persona). Columna nueva socio_id en rec_presencia.supabase-api.js, app.js. SW v142 (visible v142).pagehide borraba la fila de presencia al apagar la pantalla o cambiar de app — la dinámica normal de la operación. Fix: ya no se borra en segundo plano; al volver al frente se re-marca y re-lee al instante. Ventana de vigencia ampliada de 45s a 3 minutos.supabase-api.js. SW v141 (visible v141 en la barra inferior y en "Acerca de").rec_presencia que ambos dispositivos ya transmiten presencia con la versión nueva (filas de "Diario" y "Bóveda Personal").rec_presencia) (SW v139)rec_presencia (proyecto REC) al entrar al módulo de recaudación, con latido cada 20s, y todas las apps la leen cada 5s y pintan la unión (canal + tabla, dedupe por key). Al salir se borra la fila; pagehide intenta borrarla; filas >45s se ignoran y >1h se limpian oportunistamente. Verificado con inserción/lectura real como rol anon.supabase-api.js. Tabla nueva rec_presencia (REC). SW v139 (visible 139).join/leave además de sync, repintado de respaldo cada 4s, y render al confirmar la suscripción. window.recPresRender expuesto.supabase-api.js, app.js. SW v138 (visible 138).ch.track) se enviaba antes de que el canal rec-presencia terminara de suscribirse; en ese estado el track se pierde en silencio. En diario era sistemático: su panel "Agregar" es el inicial, así que el track salía justo al entrar (canal aún conectándose) y nunca llegaba.subscribe(status) con callback — la presencia pendiente se (re)marca cuando el canal confirma SUBSCRIBED y también en cada reconexión (al volver del segundo plano). recPresEntrar/Tipo/Salir solo trackean con el canal listo; si no, queda pendiente y se envía al suscribir.supabase-api.js. SW v137 (versión visible 137).CACHE_NAME del SW y ese número visible quedó desincronizado (parecía que "no se actualizaba la versión").boveda-personal-v136). De aquí en adelante, ambos se suben juntos en cada release.index.html (versión en "Acerca de"), sw.js. SW v136.reg.waiting/updatefound→installed); (2) sin cuenta regresiva automática (eso era lo que dejaba el "actualizando" colgado); (3) al tocar Actualizar, aplica y recarga UNA sola vez garantizada (por controllerchange o respaldo de 1.5s), con guardia _swReloaded. Tras recargar ya no hay worker en espera → el banner no reaparece.skipWaiting() en install (queda en espera para poder ofrecer el banner); solo activa al recibir SKIP_WAITING del botón.index.html (banner + registro SW), sw.js (install sin skipWaiting). SW v135.#recPresenciaModal).supabase-api.js (APP = 'Bóveda Personal'; el banner también pinta dentro del modal), index.html (contenedor #recPresenciaModal). SW v134.rec-presencia (proyecto REC). recPresEntrar/Tipo/Salir (track presencia al abrir/cambiar tipo/cerrar el modal) y recPresAgrego (broadcast al guardar). Banner y toast autocontenidos. Wire: abrirModalRec/selRecTipo/cerrarModalRec/enviarRec + recPresIniciar en initApp.supabase-api.js (módulo de presencia), app.js (wire). SW v133.<head>, ahora app.js re-aplica el tema guardado al cargar usando aplicarTema(_temaActual()) (fuente única y autoritativa). Así el tema persiste aunque el init rápido del <head> fuera de una versión previa. Se verificó que nada borra propi_tema y que el indicador de botón activo existe.app.js. SW v132.<head> (que aplica el tema antes de renderizar para evitar parpadeo) tenía una lista de temas válidos desactualizada — no incluía grafito/vino/índigo. Al recargar, esos temas eran rechazados y volvía a "claro" → parecía que no se guardaba la selección.app.js (mismos _TEMAS/_TEMA_TINTE/_TEMA_COLOR): valida los 12 temas, aplica data-tinte para esmeralda/grafito/vino/índigo y data-negro para negro, y usa el color de barra correcto.index.html (init de tema en <head>). SW v131.controllerchange→reload + worker en "waiting" que reaparece).skipWaiting() en install y toma el control solo (clients.claim); la versión nueva se aplica al reabrir la app. Sin banner, nada que se quede pegado.index.html (registro SW minimal, se quita banner HTML+JS), sw.js (skipWaiting() en install). SW v130.#fff1f6, menta, durazno…) el aviso usaba un crema muy pálido (#fffbeb) casi idéntico al fondo → la tarjeta no se distinguía.#fef3c7, borde #f59e0b, texto #7c2d12) + sombra suave, para que resalte sobre cualquier fondo claro. La variante oscura se mantiene.app.js (_notaFaltantesHTML). SW v129.app.js (_notaFaltantesHTML). SW v128._temaEsOscuro(): claro sobre fondo claro (texto café oscuro) y oscuro sobre fondo oscuro (texto dorado). Legible en ambos.app.js. SW v127.#001723 casi negra, grilla #e7e8e9 clara) que en temas oscuros eran invisibles. Ahora el renderStatsChart toma los colores del tema (variables CSS --lm-primary/accent/border/muted), así se ve bien en claro y en todos los oscuros. También la leyenda de "Proyección" usa el acento del tema.app.js (colores del gráfico por variables de tema), index.html (leyenda del detalle + swatch de proyección theme-aware). SW v126.data-theme="oscuro" + un data-tinte, y definen sus variables --lm-*/--pm-* para que el dashboard premium también se re-tinte. Heredan --pm-text claro del tema oscuro → buen contraste (texto claro sobre fondo oscuro).resume() del AudioContext (que en móvil arranca "suspended") antes de reproducir, así se escucha la muestra al seleccionar.app.js (temas + _temaEsOscuro + reproducirSonidoNotif con resume), app.css (tintes grafito/vino/indigo + regla de contraste), index.html (3 botones de tema). SW v125.abrirModalRec(fecha) acepta una fecha opcional (valida YYYY-MM-DD, si no, usa hoy); los chips del aviso llaman abrirModalRec('YYYY-MM-DD').app.js (abrirModalRec con fecha, chips tocables en _notaFaltantesHTML). SW v124.homePremium justo debajo de la tarjeta de balance (antes de "Accesos Rápidos"). Como ese layout tiene fondo oscuro fijo, el aviso del home usa siempre la variante oscura. El historial clásico sigue adaptándose al tema.index.html (contenedor movido dentro de homePremium), app.js (aviso home oscuro fijo). SW v123._temaEsOscuro: oscuro/negro/esmeralda) y usa la variante oscura (ámbar translúcido + texto dorado) en el home y en el historial clásico. En temas claros mantiene el ámbar claro. La vista premium ya usaba la variante oscura.app.js (_temaEsOscuro, aviso adaptativo). SW v122._diaSemana, _calcularDiasFaltantes, _notaFaltantesHTML; contenedor #faltantesAvisoHome en tab-home. Se recalcula en cada refresh().index.html (contenedor del aviso en home), app.js (helpers + inyección en historial/home + día de semana). SW v121.localStorage: propi_sonido_notif).assets.mixkit.co, que podía no sonar sin conexión). showToast ahora llama reproducirSonidoNotif().index.html (tarjeta + #sonidoNotifOpciones), app.js (motor de audio, selector, reemplaza el Audio de mixkit). SW v120.pmFiltrarAntAnio/pmFiltrarAntMes sobre _histAnticiposData, acordeón pmTogglePeriodo, chips de filtro en estilo oscuro. Reutiliza _extraerAnio/_extraerMes/_fmtPeriodoLabel. La versión clásica ya tenía estos filtros; ahora la premium también.index.html (barras de filtro en pmHistAntView), app.js (pmRenderAntAnt colapsable + lógica de filtros). SW v119.getHistorialCompletoSocio ahora se arma únicamente con Supabase (anticipos_historial = archivado + anticipos = activo). Sheets/GAS pasa a ser solo respaldo: solo se usa si Supabase no tiene NINGÚN registro para el socio (datos aún no migrados). Se mantiene la dedup por fecha+monto por si un anticipo quedó en activo y archivado a la vez.supabase-api.js. SW v118.getHistorialCompletoSocio junta 3 fuentes (archivado anticipos_historial + activo anticipos + GAS). Deduplicaba GAS contra el archivado, pero no los activos contra el archivado, así que un anticipo que quedó en ambas tablas se mostraba dos veces (fecha+monto repetidos), inflando también el total del período.responsable si alguna fuente lo trae. Corrige tanto la lista como el total del período.supabase-api.js. SW v117.folder y chevron) debajo de la tarjeta de datos.docFileInput/docUploadBtn/docsLista, así que subir/listar/borrar funciona igual). La lista se refresca al abrir.index.html (opción en Perfil + documentosModal; se quita el bloque de Configuración), app.js (abrirDocumentosModal/cerrarDocumentosModal). SW v116.settings, pestaña tab-config).flex-1 para que quepan). Se conservan abrirAjustesModal/cerrarAjustesModal como compatibilidad (la primera ahora navega a la pestaña, la segunda es no-op). Los IDs internos no cambiaron, así que tema/notificaciones/documentos siguen sincronizándose igual desde renderPerfil().index.html (mueve el bloque a tab-config, nuevo botón de nav), app.js (abrirAjustesModal→switchTab('tab-config')). SW v115.getSocios y getDiasPartTime. Al reanudar en el móvil la conexión queda "dormida" y el primer fetch se cuelga hasta el timeout de red del sistema (~30s); como iban dentro del Promise.all del balance, congelaban todo el refresh. Fix: ambas ahora usan _conTimeout (8s) → nada bloquea más de 8s._stale, y refresh() conserva los últimos datos buenos (_lastGood) en vez de pintar la pantalla en blanco → la data se queda visible y se actualiza sola.refresh() no tenía guardia; al volver, el evento de visibilitychange + el intervalo de 20s + el realtime disparaban varios refrescos a la vez. Fix: guardia de reentrada con coalescing (si ya hay uno corriendo, se marca pendiente y se ejecuta uno solo de seguimiento).supabase-api.js (timeouts + marca _stale en get, getAllDataDesdeSheets, getDiasPartTime, getSocios), app.js (refresh con guardia + _lastGood). SW v114.Promise.all del balance → la app parecía pegada._conTimeout que limita cada llamada: las consultas Supabase del balance (getAllDataDesdeSheets, getSaldosAnteriores, get de recaudaciones) y sus respaldos al GAS ahora tienen timeout (6–8s). En el caso normal Supabase responde al instante; si algo se demora, la app sigue con lo que hay y se actualiza en segundo plano — nunca se congela 30s.supabase-api.js. SW v113.transform al sheet del modal; eso, junto al mix-blend-mode del logo, rompía la composición y volvía el logo (y su zona) blanco en tema oscuro. El isolation:isolate agregado en v111 lo empeoraba.transform del sheet y el isolation. Ahora tocar la raya cierra y deslizarla hacia abajo también (detección de gesto sin animar el sheet), así el logo nunca se rompe y sigue fundiéndose con el tema como antes.index.html (sin isolation en #aboutSheet/#loginOverlay), app.js (_initAboutSwipe sin transform). SW v112..marca-logo) usa mix-blend-mode para fundirse con el tema. En algunos dispositivos, con el backdrop-blur del modal, la fusión no resolvía bien y el logo podía "perderse". Se agregó isolation:isolate al sheet del modal Acerca de y al login, para que el logo se funda de forma confiable contra el fondo de la tarjeta en todos los temas. (El logo no se había quitado; solo se blindó su fusión.)index.html (#aboutSheet, #loginOverlay). SW v111.index.html (#aboutHandleZone con onclick + #aboutSheet), app.js (_initAboutSwipe con gestos touch). SW v110.carlospn-logo.png, clase .marca-logo que se funde con el tema) al pie de la pantalla de login, con la línea "© 2026 · Todos los derechos reservados". No interfiere con el ingreso (pointer-events:none).index.html (bloque marca en #loginOverlay). SW v109.beforeinstallprompt).index.html (botón + #installGuideModal), app.js (abrirGuiaInstalar, _instalarPWA, _maybeMostrarGuiaInstalar, beforeinstallprompt). SW v108._homeVersionGuardada() siempre devuelve premium y el selector "Versión de inicio" en Ajustes quedó oculto (la clásica se retira)..pm-switch-fx). Se marca propi_premium_migrado para no repetirlo. Quienes ya estaban en premium pasan directo, sin modal.index.html (modal #nuevaVersionModal, selector de versión oculto), app.js (_maybeMigrarPremium, _activarNuevaVersion, _homeVersionGuardada/setHomeVersion forzados a premium), app.css (animación pmSwitchFx). SW v107.default) y no lo pospuso. Botón "Activar" → pide el permiso y, al aceptar, queda suscrito automáticamente (llama a activarNotificaciones). Botón "Ahora no" lo pospone 3 días.index.html (#notifPrompt), app.js (_maybeShowNotifPrompt, _notifPromptActivar, _notifPromptCerrar; se refresca desde _pushRefrescarEstado). SW v106.app.js. SW v105.mix-blend-mode: multiply (el blanco se vuelve transparente, el logo azul marino se ve); temas oscuros (oscuro/negro/esmeralda) filter: invert(1) hue-rotate(180deg) + mix-blend-mode: screen (el blanco → negro → transparente, y el diseño queda claro y visible). Verificado numéricamente: el fondo se funde exacto con el color del tema y el logo queda como marca de agua.app.css (regla .marca-logo). SW v104.app.css (regla .marca-logo). SW v103.--lm-card y las variables premium (--pm-card, etc.) no se sobreescribían y algunas reglas de negro quedaban pisadas por las de oscuro. Ahora un bloque al final de app.css (gana por orden de fuente) pone todas las tarjetas negras (#141414) con borde y texto de contraste. También se corrigió el contraste de las tarjetas ámbar/verde de días PT en temas oscuros (texto más claro).data-tinte), Menta (verde-agua claro) y Durazno (cálido claro). Total: 9 temas.app.css (overrides de negro + 3 temas nuevos + contraste PT oscuro), app.js (_TEMAS, _TEMA_TINTE, aplicarTema maneja data-tinte), index.html (botones de tema + script anti-parpadeo actualizado). SW v102.WHEN NEW.estado='PENDIENTE').push-notify v5 (helper push() reutilizable, doble envío para dias_pt_solicitados), trigger trg_notify_push_dia_pt (INSERT OR UPDATE). Requiere que el socio tenga las notificaciones activadas en su app.index.html (#ptConfTotalVPCl / #ptConfTotalVPPm), app.js (renderPTConfirmarCard suma VP confirmado + pendiente). SW v101.app.js (línea VP en el detalle de días pendientes). SW v100.index.html (#ptConfTotalEstCl / #ptConfTotalEstPm), app.js (renderPTConfirmarCard calcula el total). SW v99.index.html (#ptConfirmarCardClasica, #ptConfirmarCardPm, botón #calMinBtn, #calGridWrap), app.js (renderPTConfirmarCard, toggleCalMinimize). SW v98.index.html (bloque #calPTResumen), app.js (_ptValorDia, cálculo del resumen). SW v97.~$…, calculado con la recaudación de ese día × sus puntos).PENDIENTE en la tabla dias_pt_solicitados (proyecto socios). La administración recibe una notificación push ("Día Part-Time por confirmar") aunque tenga la app cerrada.dias_pt) y en la app del socio cambia de ámbar a verde (confirmado) con su valor definitivo. Si lo rechaza, el socio ve el motivo y puede volver a marcarlo.PENDIENTE). No se permite marcar días futuros ni días ya confirmados.dias_pt_solicitados y dias_pt).supabase-api.js (acciones marcarDiaPT, desmarcarDiaPT, misDiasPTSolicitados + realtime), app.js (calendario interactivo, togglePTDia, cargarDiasPTSolicitados, detalle "por confirmar"/"rechazados"), app.css (estilos .pt-pendiente / .pt-rechazado), index.html (hint del calendario). Backend: tabla dias_pt_solicitados + trigger push, edge function push-notify v4. SW v96.cancelarEgreso (Supabase): borra solo las solicitudes con estado PENDIENTE del socio (nunca las ya procesadas) y avisa a socios-comicion por realtime, así deja de aparecer en el panel de administración.supabase-api.js (acción cancelarEgreso), app.js (botón + cancelarEgreso()). SW v95.#pmDetallesContables (Resumen Contable con estilo oscuro) y #pmEgresoEstadoBox (egreso pendiente). El JS ahora llena ambas versiones (clásica y premium) con la misma información: Total Bruto, Saldo Anterior, Descuentos/Anticipos, y la tarjeta de egreso pendiente.index.html (contenedores premium), app.js (renderEgresoEstado y resumen contable llenan premium). SW v94.index.html (botón Temas, contenedores #helpIndex/#helpContent/#helpNav), app.js (helpToggleIndex, helpRenderIndex, helpIndexGoTo + textos actualizados). SW v93.https://diario-propi.vercel.app/) en una pestaña nueva, para acceder al diario completo de recaudaciones desde la app del socio.index.html. SW v92.data-negro que empuja los fondos a negro puro (OLED), reutilizando todo el restyle oscuro sin duplicar CSS.mix-blend-mode:multiply); en Oscuro/Negro se invierte (invert + hue-rotate) para que el fondo del logo se vuelva oscuro (mimetizado) y el diseño quede claro y visible.app.css (tema negro + logo), app.js (aplicarTema, _TEMAS, _TEMA_COLOR), index.html (botón, script pre-render, clase marca-logo). SW v91.#loginCTA) estaba absolute bottom-8 ocupando todo el ancho abajo, y se interponía con el botón "Activar Dispositivo" del setup.index.html. SW v90.img/carlospn-logo.png) arriba de la marca en el modal Acerca de, reemplazando el ícono code placeholder. Debajo queda el subtítulo "Sistema Profesional de Gestión · 2026".index.html, img/carlospn-logo.png (nuevo). SW v89.fecha+monto): Supabase es la fuente de verdad y de GAS solo se agregan los anticipos que no estén ya en Supabase. No se deduplica dentro de una misma fuente, así que no se pierden dos anticipos iguales legítimos del mismo día.supabase-api.js (getHistorialCompletoSocio). SW v88.supabase-api.js (getHistorialCompletoSocio). SW v87.getHistorialCompletoSocio leía el histórico solo desde GAS, pero el archivado actual (al marcar Cobrado o al cerrar el mes en socios-comicion) se guarda en la tabla Supabase anticipos_historial, no en GAS.anticipos_historial de Supabase y se fusiona con lo de GAS (evitando duplicar períodos que ya vengan de GAS)._fmtPeriodoLabel): "CIERRE_JULIO_DE 2026", "JULIO_2026" o "2026-06-15" → "Julio 2026".supabase-api.js (lectura de anticipos_historial), app.js (_fmtPeriodoLabel en las 2 vistas de anticipos anteriores). SW v86.MOVS_MAX (app.js) — fácil de ajustar.index.html (badges #anticiposCount / #pmMovsCount), app.js (actualiza los contadores con merged.length). SW v84.allSocios), y el socio destacado sigue viendo "⭐ PARA TI" + anillo dorado.notas_recaudacion.destacados). Solo aparece en Soporte (que es donde viven las notas admin), no en Equipo/Admin (son otros sistemas de mensajes).app.js (badge "Destacado para" en render del chat Soporte). SW v83.vercel.json usaba un source con grupo/alternación "/(index.html|app.js|app.css|supabase-api.js|originalindex.html)". Ese patrón puede fallar la validación de Vercel y romper el deploy, dejando la app servida en la última versión buena (por eso desde ~v78 los cambios no se reflejaban en el dispositivo).vercel.json con una regla por archivo con ruta exacta (/index.html, /app.js, /app.css, /supabase-api.js, /), 100% válidas para Vercel. Mismo arreglo en diario.propi. (socios-comicion ya usaba /js/(.*), patrón válido, y sí desplegaba.)diario.propi en una pestaña nueva (https://diario-propi.vercel.app/).index.html (marcada con comentario) — ajustar si el dominio de despliegue es distinto.index.html (enlace en el modal de Ajustes). SW v81.notas_recaudacion.destacados (IDs de socio); si el currentUser.ID está en la lista, se resalta la burbuja.supabase-api.js (getNotes REC devuelve destacados), app.js (resaltado en el render del chat ADMIN/Soporte). SW v80.env(safe-area-inset-bottom), se agregó un div espaciador (90px + área segura) al final de #homePremium (tras "Últimos Movimientos") y de #historyPremium. Así el último ítem (ej. el primer anticipo con su fecha) nunca queda tapado por la barra inferior en ningún teléfono, incluso en casos límite de flex/scroll.index.html (espaciadores). SW v79.index.html/app.js/app.css/supabase-api.js, y el updater viejo no lograba refrescarlas.vercel.json para servir esos archivos core (y /) con Cache-Control: no-cache, must-revalidate (antes solo sw.js). Así el navegador revalida siempre y baja la versión nueva en la próxima apertura, sin necesidad de recarga forzada.vercel.json. SW v78.#homePremium y #historyPremium se cambió min-height:calc(100vh - 120px) (el 100vh varía en iPhone/Android y descuadraba el scroll) por min-height:100%, y el padding inferior pasó a calc(110px + env(safe-area-inset-bottom)). Así el contenido siempre llega al final y libra la barra inferior en todos los modelos (iPhone con notch, Redmi, etc.).index.html (padding/min-height premium + novedades del banner), app.js (helpSlides). SW v77.controllerchange) la app recarga UNA vez para aplicar la versión (sin recargar en la instalación inicial). Antes dependía de una bandera (_updateApplying) que podía no dispararse.installing al cargar (no solo waiting), así el banner aparece en más casos. Red de seguridad: si controllerchange no dispara en 2.5 s, recarga igual.cache:'no-store', así el navegador/CDN no puede servir una copia vieja → la versión nueva baja de verdad.index.html (_seguirWorker, controllerchange con guardia, _aplicarActualizacion), sw.js (fetch no-store para core). SW v76.#correoModal, ~3s después de entrar; espera si el modal de RUT/ayuda está abierto).socios.correo, acción guardarCorreoSocio en supabase-api.js, con auditoría). El mapeo getSocios ahora incluye Correo.supabase-api.js (guardarCorreoSocio + Correo en getSocios), index.html (#correoModal + fila Correo en el perfil), app.js (checkCorreoRequired, openCorreoModal, submitCorreo, poblar #perfilCorreo). Migración Supabase: add_correo_to_socios. SW v75.w-20→w-32), con el ícono/placeholder acorde.index.html (selector movido al modal, avatar login w-32), app.js (_aplicarLoginShape en renderPerfil). SW v74.#fastPIN y "Crear PIN" #setupPIN) para mostrar/ocultar el PIN. Alterna type password/text y el ícono visibility/visibility_off.propi_login_shape).index.html (ojitos + selector de forma), app.css (.login-shape-btn), app.js (togglePinVisible, setLoginAvatarShape, _aplicarLoginShape). SW v73.--pm-* (fondo, tarjeta, texto, acento, verde, rojo, borde) definidas por tema, y reglas .pm-layout [style*=...] que mapean los colores fijos del layout premium a esas variables. Las secciones con skin (Estadísticas/Mensajes/Perfil) ahora siguen el tema por su cuenta (se quitó el forzado oscuro data-premium).app.css (variables --pm-* + mapeo .pm-layout), index.html (clase pm-layout en #homePremium y #historyPremium). SW v72.:root[data-premium][data-premium][data-theme="X"] que cambian las variables de acento (--lm-primary/--lm-accent/--lm-secondary) para las secciones con skin, y overridean los colores de acento fijos del layout premium (#cee6f7 celeste y #6366f1 índigo de los íconos del Perfil) al color del tema. Oscuro y Claro mantienen el acento azul/celeste por defecto.app.css (acento premium por tema). SW v71._refrescarTarjeta leyendo los puntos del socio (#perfilPuntos). IDs nuevos: tarjetaPuntos / tarjetaPuntosPm.index.html (tarjeta Valor Punto + columna Mis Puntos en ambas tarjetas bancarias), app.js (_refrescarTarjeta). SW v70.pmPuntos, pmValorPunto, pmRemanente).index.html (grid de stats premium a 2 columnas). SW v69.visibilitychange). Si quedaba abierta en primer plano, nunca se chequeaba → el banner no aparecía y había que recargar a mano (Ctrl+R).updateViaCache:'none' (nunca cachea sw.js por HTTP) y se agregó un chequeo periódico cada 45 s (reg.update()) mientras la app está abierta. Así el banner aparece solo poco después de publicar una versión.index.html (registro SW + setInterval de chequeo + cuenta regresiva recarga). SW v68.globalDiasCalendar.length === 0), igual que en la clásica (para Planta sin ausencias no aparece).#pmBalanceTarjeta) y _aplicarBalanceEstilo / _refrescarTarjeta ahora manejan los dos juegos de tarjetas (clásica y premium) con la misma preferencia (propi_balance_estilo).index.html (#pmBalanceCard, #pmBalanceTarjeta, botón 💳), app.js (_aplicarBalanceEstilo/_refrescarTarjeta para ambas versiones, #pmCalBtn condicional, monto premium en vivo). SW v67.:root[data-theme="oscuro"] [style*=...]) tenían más especificidad que el skin premium por clase, así que ganaban. Estadísticas cambiaba (usa variables) pero Perfil y Mensajes (usan estilos inline #fff/#001723) se quedaban con los colores del Oscuro.body a un atributo data-premium en <html>, con selector :root[data-premium][data-premium] … (atributo repetido) para garantizar mayor especificidad que las reglas del tema Oscuro. Ahora Perfil y Mensajes sí toman la paleta premium.app.css (reglas :root[data-premium][data-premium]), app.js (_aplicarHomeVersion set/removeAttribute data-premium). SW v66.#tab-chat, que tapaba el header (foto, campana, área, ayuda, cerrar) que se ve por el espaciador superior del chat. Ahora el skin se aplica al body (no al chat directo), así el header vuelve a verse..premium-skin al body, el header (.glass) y la barra de navegación (#bottomNav) toman las variables premium (--glass-bg / --nav-bg / --nav-border), quedando en el tono oscuro premium.#0f1419, tarjetas #171c22, verde #3de273) en vez de la paleta oscura estándar, para que se note el cambio respecto al tema Oscuro. Se mantiene el primario azul para no romper el contraste de botones/tarjetas con texto blanco.app.css (.premium-skin con variables de header/nav + tonos mockup), app.js (_aplicarHomeVersion aplica el skin al body). SW v65..premium-skin que redefine la paleta (variables --lm-*, --color-card, --chat-bg, etc.) y convierte los estilos inline claros a oscuros, sólo en esas tres pestañas y sólo cuando la versión Dashboard está activa. Usa los colores del tema oscuro (probados) para no romper contrastes._aplicarHomeVersion ahora: muestra los layouts premium de inicio/historial y aplica/quita .premium-skin en #tab-stats, #tab-chat, #tab-perfil. Al volver a "Clásica", todo regresa al tema de color elegido.app.css (.premium-skin), app.js (_aplicarHomeVersion). SW v64.app.js (reconstrucción del bloque premium del historial con tiposHTML), index.html (#pmHistList sin caja, cada día es su propia tarjeta). SW v63.loadHistorialAnticipos → getHistorialCompletoSocio) y los muestra en estilo oscuro: cada período como tarjeta con su total y el detalle de registros (fecha, responsable, monto). Se carga la primera vez que se abre.index.html (toggle + #pmHistMovsView / #pmHistAntView / #pmAntAntList), app.js (pmSwitchHist, pmRenderAntAnt). SW v62.#historyClasico y se agregó #historyPremium. _aplicarHomeVersion alterna ahora ambas secciones (inicio + historial) a la vez.index.html (#historyPremium), app.js (poblado de pmHist* en el cálculo, toggle por pares en _aplicarHomeVersion). SW v61.propi_home_version).#homeClasico; la nueva vista es #homePremium (oscura, hardcodeada, independiente del tema de color).index.html (#homePremium, selector en el modal de Ajustes), app.js (setHomeVersion, _aplicarHomeVersion, _refrescarPremium, poblado de pm* en el cálculo del balance), app.css (.homever-btn). SW v60.#001723 fijo). Ahora usa rgb(var(--lm-primary)), así resalta en todos los temas._TEMAS/_TEMA_COLOR en app.js, bloques de variables en app.css.)propi_balance_estilo). El botón 🔄 en la tarjeta vuelve a la clásica.app.css (.nav-btn.active, temas aqua/lavanda), index.html (botones de tema, #balanceTarjeta, botón alternar), app.js (toggleBalanceEstilo, _aplicarBalanceEstilo, _refrescarTarjeta, sync del monto). SW v59.index.html (engranaje abrirAjustesModal(), #ajustesModal, filas de antigüedad en Info Laboral), app.js (abrirAjustesModal/cerrarAjustesModal; se quitó togglePerfilAjustes), app.css (se quitó la regla de la barra). SW v58.currentUser.Rut y, si no viene, usa el RUT guardado en el login del dispositivo (visor_secure_auth), formateado (12.345.678-9).app.css (#perfilAjustesBtn en [data-theme="oscuro"]).index.html (fila RUT), app.js (poblar #perfilRut). SW v57.index.html (botón #perfilAjustesBtn + contenedor #perfilAjustesPanel envolviendo los 4 bloques), app.js (togglePerfilAjustes). SW v56.documentos (categoria='socio', socio_id, subido_por). El socio ya filtra por su socio_id, así que el documento aparece automáticamente.app.js (renderDocumentos: distingue subido_por ≠ 'socio'). SW v55._rec_last_seen, _social_last_seen, _admin_priv_last_seen; se inicializan a "ahora" la primera vez para no contar el historial).index.html (campana + menú), app.js (_getUnread, renderNotifBell, toggleNotifMenu, _irAMensaje; enganchado a los ciclos de refresco). SW v53.visibilitychange hacía pingConexion() (enviaba "conectado" a Telegram) antes de verificar la inactividad, mostrando una reconexión falsa cuando en realidad la sesión se cerró por inactividad.logout() ahora envía la desconexión (logoutConexion) antes de limpiar el usuario, para que Telegram refleje el cierre.app.js (handler visibilitychange + logout). SW v52.checkSecurity auto-entra si la sesión sigue viva (PIN de sesión en sessionStorage), salvo que haya expirado por inactividad (≥15 min). El cierre por inactividad/manual (logout) ahora borra el PIN de sesión, así tras inactividad sí se pide PIN (seguridad intacta).controllerchange (con red de seguridad a 3.5 s) y no reaparece.controllerchange solo ocurre si el usuario tocó "Actualizar".app.js (checkSecurity auto-login + logout limpia sesión), index.html (_actualizarSilencioso, _aplicarActualizacion, controllerchange guardado). SW v51.socId). Si el socio no tiene foto, se mantiene la inicial.app.js (renderUsersList, selectSocialTarget, avatar en renderChat). SW v50.index.html (dos inputs con/sin capture + dos botones), app.js (reset de ambos inputs). SW v49.avatares (carpeta chat/).addNote (Soporte→notas_recaudacion, Equipo→chat_mensajes) y sendAdminMsg (mensajes_admin) ahora guardan/leen foto_url; los mapeos de getNotes/getAdminMsgs devuelven foto.index.html (botón + preview), app.js (adjuntar/subir/enviar/render), supabase-api.js (handlers). SW v48.notas_recaudacion.foto_url).supabase-api.js (mapea foto en getNotes REC), app.js (render de la imagen en el chat). SW v47.#fotoGrandeOverlay + verFotoGrande(), cerrarFotoGrande(), tapPerfilAvatar(), _authFoto().index.html (overlay + onclicks), app.js. SW v46.#loginCTA) arranca oculto y _toggleLoginCTA() lo muestra solo si no existe auth.id en visor_secure_auth. Se re-evalúa en checkSecurity, switchToSetup y cancelRecovery.index.html (id="loginCTA" + oculto por defecto), app.js (_toggleLoginCTA). SW v45.push_subscriptions (proyecto socios). Un trigger en mensajes_admin (pg_net) llama a la Edge Function push-notify, que envía el push con web-push a las suscripciones del socio.push (muestra la notificación) y notificationclick (enfoca/abre la app).sw.js (handlers push + v44), app.js (suscripción + botón + helpSlide), supabase-api.js (savePushSub/deletePushSub), index.html (botón en Perfil). Backend: tabla push_subscriptions, Edge Function push-notify, trigger en mensajes_admin. SW v44.renderChatPriv/handleSendPriv/refreshAdminPriv) para no tocar la lógica de Soporte/Equipo.mensajes_admin (proyecto socios, RLS anon, realtime).index.html (botón modo + dot), app.js (render/send/refresh privados + helpSlide), supabase-api.js (acciones getAdminMsgs/sendAdminMsg). SW v43.helpSlides), después de "Anticipos y Descuentos":
preview visual acorde al estilo de la guía.app.js (helpSlides). SW v42.solicitudes_egreso (proyecto socios). En la app queda una tarjeta de estado ("Egreso solicitado · pendiente") mientras la administración no la procese.index.html (botón + modal + tarjeta estado), app.js (abrirModalEgreso, enviarEgreso, renderEgresoEstado), supabase-api.js (acciones solicitarEgreso y miSolicitudEgreso). Nueva tabla Supabase solicitudes_egreso (RLS anon, realtime). SW v41.helpSlides), entre "Perfil" y "Seguridad":
preview visual acorde al estilo de la guía.app.js (helpSlides). SW v40.color:inherit), por lo que el texto de los mensajes quedaba casi invisible (letras claras sobre burbuja clara).--chat-bg por tema (Claro beige, Rosa rosado, Oscuro #0b141a). En Oscuro se agregaron reglas de alta especificidad (#chatMessages ...) que fuerzan estilo tipo WhatsApp dark: burbujas #202c33/#005c4b, texto #e9edef, autor #53bdeb, hora #8696a0, colas, separador de fecha, banner fijado y barra de entrada.app.css. SW v39.getNotes de supabase-api.js (chat Soporte → notas_recaudacion, chat Equipo → chat_mensajes) hacían const { data } = await ... ignorando el error. Si Supabase fallaba un instante (los proyectos tuvieron pausas/restauraciones recientes), la consulta devolvía error y el handler entregaba [], dejando el chat vacío hasta el siguiente refetch exitoso._lastNotasRec / _lastChatSocial) en vez de vaciar el chat. Un hipo transitorio ya no borra la conversación en pantalla.anon: Soporte 59 mensajes, Equipo 4. El tema (Claro/Oscuro/Rosa) no afecta la carga del chat.supabase-api.js. SW v38.localStorage (propi_tema) y se aplica automáticamente al abrir la app, en cualquier dispositivo donde se haya elegido.lm-*/wa-* se convirtieron a variables CSS con canales RGB (rgb(var(--lm-x) / <alpha-value>)), conservando los modificadores de opacidad (bg-lm-primary/10). Cada tema redefine esas variables con :root[data-theme="claro|oscuro|rosa"].#fff, textos #001723, inputs, divisores) mediante reglas de mayor peso, para que Perfil y modales se vean correctamente en oscuro.<head> (evita el parpadeo/FOUC) y se actualiza el theme-color de la barra del navegador según el tema.index.html (config Tailwind con variables, script early-theme, tarjeta selector en Perfil), app.css (variables :root por tema + overrides de oscuro + estilos del selector), app.js (window.aplicarTema, _temaActual, _sincronizarTemaBtns). SW v37.APP_NOVEDADES, editable por release).skipWaiting automático en install: espera el mensaje SKIP_WAITING (del botón o del temporizador) para activar, así no reinicia sin avisar. Fallback de recarga por si no dispara controllerchange.index.html (banner arriba + lógica 15s), sw.js (install sin skipWaiting + listener de mensaje). SW v36.propi_info_perfil_v1.index.html (modal), app.js (checkInfoPerfil/cerrarInfoPerfil/irAMiPerfil). SW v35.visor_secure_auth.foto) para poder mostrarla en la pantalla de ingreso antes de cargar los datos.index.html (id en avatar del login), app.js (_aplicarFotoPerfil cubre header+login, checkSecurity muestra la foto). SW v34.capture=user) o Elegir de galería (input sin capture).index.html (dos inputs + menú), app.js (abrirMenuFoto/cerrarMenuFoto). SW v33.avatares, ruta socio/<id>.<ext>, con upsert que reemplaza la anterior) y la URL pública queda en socios.foto_url. Se refleja en socios-comicion (Gestión de Socios).guardarFotoSocio. Archivos: index.html (avatar+cámara), app.js (subirFotoPerfil/_aplicarFotoPerfil), supabase-api.js (map FotoUrl + handler). SW v32.documentos, base de socios) con metadatos en la tabla documentos. Visibles/recuperables desde socios-comicion.dbSV.storage (subida, URL firmada 1h para ver, borrado). Límite 15 MB por archivo.index.html (apartado en Perfil), app.js (cargar/subir/ver/borrar). SW v31.11.111.111-?), explicando que se usará para informes y certificados. Se pre-llena con el RUT de recuperación local si existe.socios.rut) vía acción guardarRutSocio; queda disponible en socios-comicion (Gestión de Socios y Certificados). El evento queda en auditoría.index.html (modal), app.js (checkRutRequired/submitRut), supabase-api.js (handler + map Rut). SW v30.auditoria de la base de socios, dbSV), con la acción "Registrar Recaudación".registrado_por_nombre / registrado_por_id), tipo, fecha, monto y divisor. Aparece en el historial de auditoría de socios-comicion con origen propi.solicitada.supabase-api.js. SW incrementado a v29.allSocios con el mismo criterio que socios-comicion: solo socios visibles/activos (regla del día 15 según FechaInicioPuntos), usando el valor de BD (Puntos) si es > 0 o la fórmula por antigüedad/área si no. Sirve como referencia al ingresar el divisor._recCalcTotalPuntos() en app.js; se actualiza al abrir el modal.app.js e index.html.addRecaudacion de supabase-api.js insertaba solo id, fecha, tipo, monto en la tabla recaudaciones e ignoraba por completo b.divisor. El divisor vive en una tabla aparte (divisores, una fila por fecha) que el handler get mergea por fecha, pero nunca se escribía ahí. Por eso el divisor no subía a Supabase ni aparecía en las otras apps (diario.propi, socios-comicion) que leen de la misma tabla.addRecaudacion ahora hace upsert en divisores ({ fecha, valor }, on conflict fecha) cuando llega b.divisor > 0. Las tres apps comparten el mismo proyecto Supabase de recaudaciones, así que el divisor ahora se propaga a todas.supabase-api.js.|| 1 como fallback, dividiendo el monto por 1 y sumando el total completo al VP acumulado del socio, dando cifras elevadas e incorrectas.|| 1 → || 0 con guarda if(d > 0) en los dos lugares donde se calcula VP (balance histórico y detalle de ganancias). Sin divisor, el día no contribuye al VP.> 1 a > 0, permitiendo guardar divisores válidos como 1 ó 1.5 que antes se descartaban.app.js.Promise.allSettled([Supabase, GAS]) bloqueante. El Promise.all del balance en app.js esperaba al más lento (GAS: 2-5 s) antes de mostrar cualquier dato.getDiasPartTime ahora responde inmediatamente con datos de Supabase. GAS va en background solo en la primera carga de sesión; si detecta socios PT faltantes en Supabase, los guarda en un cache de sesión y dispara un refresh único para actualizar el balance. En cargas posteriores del mismo sesión usa el cache sin volver a llamar GAS.supabase-api.js.getDiasPartTime en supabase-api.js leía SOLO Supabase. Si la tabla dias_pt no tenía todos los socios PT (porque no habían guardado días desde la nueva app), aparecían con 0 días → sin calendario ni alcance.supabase-api.js.getSaldosCierre ya no bloquea el Promise.all del balance: retorna {} inmediato (o caché 24h) y recalcula el balance en background cuando llegan los datos de GAS.getAllDataDesdeSheets ahora migra anticipos/extras desde Sheets a Supabase la primera vez que detecta Supabase vacío (_migrarASupabase). A partir de entonces, Supabase es la fuente primaria.supabase-api.js no tenía fallback a GAS cuando la tabla anticipos de Supabase está vacía (anticipos aún en Google Sheets). getAllDataDesdeSheets devolvía datos vacíos y la sección "Últimos Movimientos" quedaba en blanco.supabase-api.js.linkify() ahora escapa HTML entities antes de convertir URLs, bloqueando inyección de HTML/JS en mensajes del chat.msgContent en el atributo. Se usa window._chatMsgMap para lookup por ID, eliminando template injection.showDebugToast escapa todos los valores del objeto API antes de insertarlos en innerHTML.linkify() bloquea protocolos javascript: y data: en URLs.document.write() del comprobante PDF escapa nombre, área y contrato del usuario.integrity SHA-512 (SRI).getSaldosAnteriores ahora lee de saldos_socio en Supabase (67 filas, fuente de verdad); GAS solo como fallback si Supabase está vacío.getHistorialCompletoSocio ahora lee de anticipos_historial en Supabase (256 filas) + anticipos activos; GAS solo como fallback si no hay datos para el socio.anticipos_historial y saldos_socio.anticipos_historial en Supabase, que nunca se pobla (el archivado va a Google Sheets).getHistorialCompletoSocio en supabase-api.js ahora llama al GAS (getHistorialCompletoSocio), que lee todas las hojas Anticipos_* y AnticiposGuardados en Google Sheets.getSaldosAnteriores leía de Supabase (saldos_socio) que podía estar desactualizado; getSaldosCierre retornaba vacío.currentUser.Puntos de Supabase solo si es > 0; si no, caen a fórmula.boveda-personal-v14.pts = 4 + (años × 2) localmente, ignorando currentUser.Puntos de Supabase.currentUser.Puntos si está definido; solo cae a fórmula si el campo viene vacío (socios sin dato en Supabase).boveda-personal-v14.propi_last_active), actualizado en cada click/touch/scroll/key. Un setInterval de 60 s verifica si pasaron 15 min desde la última interacción → cierra sesión. Al volver de segundo plano también verifica inmediatamente.boveda-personal-v14.getActivePeriodo(mapVP): nuevo helper que detecta si el período actual (15→14) tiene datos; si no, usa el período anterior.renderStats, renderStatsChart, renderStatsTable: todas usan getActivePeriodo en lugar de getPeriodo directo.boveda-personal-v11.boveda-personal-v10.supabase-api.js agregado a la lista de precaché del SW (era el único archivo clave sin cobertura offline).backend.gs en socios-comicion ahora escribe en Supabase via UrlFetchApp (server-side) después de escribir en Sheets. Funciones afectadas: registrarBatchAnticipos, registrarBatchExtras, borrarMovimientoGlobal, actualizarAnticipo.supabase-api.js en propi.solicitada: getAllDataDesdeSheets y getHistorialCompletoSocio ahora leen de Supabase (tablas anticipos, extras, anticipos_historial) en lugar de pasar a GAS.anticipos — actualiza la UI automáticamente cuando llega un nuevo anticipo.getHistorialCompletoSocio en supabase-api.js devolvía un array plano, pero renderHistorialAnticipos espera un array agrupado por período ({ periodo, registros } ).periodo en anticipos_historial, o calculado desde fecha para anticipos actuales), ordenado de más reciente a más antiguo.boveda-personal-v8.addRecaudacion.supabase-api.js: interceptor de window.fetch y navigator.sendBeacon que redirige transparentemente todas las llamadas a las URLs GAS hacia dos proyectos Supabase sin modificar app.js.
teemahksasdougehrcly): tablas socios, anticipos, extras, saldos_socio, dias_pt, chat_mensajes, historial_conexiones, anticipos_historial.lpulmjzboogixbdxxayo): tablas recaudaciones, divisores, notas_recaudacion.index.html actualizado para cargar @supabase/supabase-js@2 (CDN jsdelivr) y supabase-api.js antes de app.js.boveda-personal-v7 para forzar recarga del index.html modificado.gas/code.gs como referencia histórica..pt-safe-top y .pb-safe-bottom estaban referenciadas en el HTML pero nunca definidas en el CSS.app.css:
.pt-safe-top { padding-top: calc(env(safe-area-inset-top) + 0.875rem) !important }.pb-safe-bottom { padding-bottom: env(safe-area-inset-bottom) !important }html { height: 100% } y body { height: 100% } para cadena de altura correcta en iOS PWA.index.html separado en tres archivos: index.html (878 líneas), app.css (270 líneas), app.js (2435 líneas).originalindex.html: copia completa en un solo archivo, auto-generada por build.sh.build.sh: script Python que reconstruye originalindex.html desde los tres archivos separados..claude/settings.json): ejecuta build.sh automáticamente al editar cualquiera de los tres archivos.sw.js actualizado a boveda-personal-v6 — ahora cachea app.css y app.js._extraerMes, buildFiltroMeses, filtrarAnticiposMes.filtrarAnticipos actualizado: ahora resetea y reconstruye el filtro de mes al cambiar de año.getHistorialCompletoSocio — combina datos de AnticiposGuardados y hojas Anticipos_*.logoutConexion actualizado: borra UltimaConexion en la hoja Socios para garantizar notificación Telegram en el próximo login.gas/code.gs: copia completa del script de Google Apps Script como referencia de código fuente.visibilitychange.guardarDistribucion | POST | Guarda distribución del período |
addSocio / updateSocio / deleteSocio | POST | CRUD de socios |
getAuditoria / logAccionAuditoria | GET/POST | Auditoría de acciones |
getCredenciales / setCredencial / deleteCredencial | POST | Gestión de credenciales |
registrarRetiroAnticipo | POST | Registra retiro de anticipo |
getRetirosAnticipos | GET | Lista retiros de anticipos |
getHistorialAnticiposSocio | GET | Historial de anticipos de un socio |
getHistorialCompletoSocio | POST | Nuevo — combina AnticiposGuardados + hojas Anticipos_* ordenadas por período desc |
registrarMaterial / borrarMaterial | POST | CRUD de materiales |
getAllMaterialesDesdeSheets | POST | Lista todos los materiales |
archivarCarpetaEnSheets | POST | Archiva una carpeta |