fuera Karvan: se usan el Telefono y las Salas del dev

El dev ya trae Telefono (llamadas uno a uno, buzon de voz) y Salas (hasta 50,
con video, grabacion y emision). La rama quita su modulo propio de salas y
llamadas: modelo, vista, JS de cliente, CSS, rutas, credenciales TURN, pruebas
y textos.

- el router propio pasa a llamarse fork_routes.js, sin Karvan
- el audio, la camara y la emision de Android se enchufan desde main.js con
  setAudioFactory/setVideoFactory/setStreamFactory, que exporta el propio dev;
  SSB_server.js vuelve a tener su linea original
- Clear-SNH.css y modules_view.js vuelven a ser identicos a los del dev
- el workflow acampada y la config de serie cambian Karvan por Telefono y Salas
- README y guia: las llamadas son las del dev; antes de hacer un modulo propio,
  mirar si el dev ya lo tiene
This commit is contained in:
hacklab 2026-10-19 13:39:41 +02:00
parent 3937ddab1e
commit 096a7d110b
30 changed files with 266 additions and 1918 deletions

View file

@ -469,34 +469,6 @@ label.welcome-ux-option:has(input:checked) { background: var(--a26-neon) !import
/* el 📌 va en la esquina: el texto deja su hueco (el padding general de la burbuja lo pisaba) */
.chat-bubble-row .chat-message.chat-has-pin-toggle { position: relative !important; padding-right: 44px !important; }
.chat-day-chip { background: var(--a26-fg) !important; color: var(--a26-bg) !important; border: 0 !important; }
/* ---------- Karvan (karvan.css trae su paleta amarilla sobre negro) ---------- */
.karvan-intro h2, .karvan-room-title, .karvan-back { color: var(--a26-fg) !important; }
.karvan-sub, .karvan-ttl-lbl, .karvan-ttl-opt, .karvan-empty, .karvan-expire, .karvan-live,
.karvan-room-item .kr-meta, .kc-status { color: var(--a26-muted) !important; }
.karvan-room-item {
background: var(--a26-bg) !important; color: var(--a26-fg) !important;
border: var(--a26-bd) !important; border-radius: var(--a26-radius) !important;
box-shadow: 0 4px 0 var(--a26-fg) !important;
font-family: var(--a26-tit) !important; font-size: 18px !important; text-transform: uppercase;
}
.karvan-room-item:active { background: var(--a26-neon) !important; transform: translateY(3px); box-shadow: 0 1px 0 var(--a26-fg) !important; }
.karvan-room-item .kr-meta { font-family: var(--a26-txt) !important; text-transform: none; }
.karvan-room-head { border-bottom: var(--a26-bd) !important; padding-bottom: 8px !important; }
.karvan-room-title { font-family: var(--a26-tit) !important; font-weight: 400 !important; font-size: 20px !important; text-transform: uppercase; }
.karvan-msg {
background: var(--a26-bg) !important; border: var(--a26-bd-fino) !important;
border-radius: 14px 14px 14px 4px !important;
}
.karvan-msg-self { background: #e5fcff !important; border-color: var(--a26-fg) !important; border-radius: 14px 14px 4px 14px !important; }
.karvan-msg .km-from { color: var(--a26-muted) !important; font-weight: 700; }
.karvan-msg .km-text { color: var(--a26-fg) !important; }
.karvan-ttl, .karvan-compose input, .karvan-compose textarea, .karvan-invite input {
background: var(--a26-bg) !important; color: var(--a26-fg) !important; border: var(--a26-bd-fino) !important;
}
.kc-btn { background: var(--a26-bg) !important; color: var(--a26-fg) !important; border: var(--a26-bd-fino) !important; }
.kc-btn.on { background: var(--a26-neon) !important; }
.kc-btn-stop { background: var(--a26-bg) !important; color: #c4003b !important; border-color: #c4003b !important; }
.chat-reply-cancel,
.chat-message-meta .chat-reply-btn { color: var(--a26-fg) !important; }
@ -532,5 +504,4 @@ body { font-size: 18px; }
.oasis-mobile-topbar .omt-menus .oasis-menu-btn { font-size: 22px !important; min-height: 48px !important; }
.tags-header p { font-size: 16px !important; }
.card-footer .date-link, .card-footer a.user-link, a.user-link, .date-link { font-size: 15px !important; }
.karvan-room-item { font-size: 21px !important; min-height: 60px !important; }
input, textarea, select { font-size: 18px !important; min-height: 50px; }

View file

@ -1,103 +0,0 @@
/* Estilos del modulo Karvan (salas efimeras y llamadas).
*
* Viven aparte del tema para que el modulo se vea igual en cualquier tema y
* tambien en escritorio, donde OasisMobile.css no se carga. Mismo patron que
* highlight.css, que tampoco pertenece a ningun tema concreto.
*/
/* --- Reply a mensaje en chats (server-side ?replyTo=, sin JS) --- */
.chat-message-meta .chat-reply-btn { margin-left: auto; padding: 0 4px; color: #FFD700; text-decoration: none; font-size: 15px; opacity: .85; }
.chat-message-self .chat-message-meta .chat-reply-btn { margin-left: 8px; }
.chat-quote {
display: block; border-left: 3px solid #FFD700; padding: 3px 8px; margin: 0 0 5px;
background: rgba(255,215,0,.08); border-radius: 6px; font-size: 12px; line-height: 1.3;
}
.chat-quote-author, .chat-quote-author a { color: #FFD700; font-weight: 700; margin-right: 6px; }
.chat-quote-text { color: #FFDD44; }
.chat-replying-to {
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
border-left: 3px solid #FFD700; background: rgba(255,215,0,.10);
padding: 6px 10px; margin-bottom: 8px; border-radius: 8px; font-size: 12px;
}
.chat-replying-label { color: #FFD700; font-weight: 700; }
.chat-reply-cancel { margin-left: auto; color: #FFD700; text-decoration: none; font-weight: 700; font-size: 14px; }
/* ==========================================================================
FORK_IA_UX · Módulo KARVAN (mensajes temporales + WebRTC) — chat estilo burbujas
========================================================================== */
.karvan-intro h2 { color: #FFD700; margin: 0 0 6px; }
.karvan-sub { color: #FFDD44; font-size: 13px; }
.karvan-create input[type="text"] { width: 100%; box-sizing: border-box; padding: 10px 12px;
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; }
.karvan-ttl { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 10px 0; }
.karvan-ttl-lbl { color: #FFDD44; font-size: 13px; }
.karvan-ttl-opt { display: inline-flex; align-items: center; gap: 4px; color: #FFD700; font-size: 13px; white-space: nowrap; }
.karvan-create button, .karvan-compose button { padding: 10px 16px; border: 1px solid #FFD700;
border-radius: 10px; background: #FFD700; color: #121212; font-weight: 700; cursor: pointer; }
.karvan-rooms { list-style: none; margin: 8px 0 0; padding: 0; }
.karvan-rooms li { margin: 0 0 8px; }
.karvan-room-item { display: flex; justify-content: space-between; align-items: center; gap: 10px;
padding: 12px 14px; background: #161616; border: 1px solid #333; border-radius: 12px;
color: #FFD700; text-decoration: none; min-height: 48px; box-sizing: border-box; }
.karvan-room-item .kr-title { font-weight: 600; }
.karvan-room-item .kr-meta { color: #FFDD44; font-size: 12px; white-space: nowrap; }
.karvan-empty { color: #FFDD44; }
/* sala */
.karvan-room-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.karvan-back { color: #FFD700; text-decoration: none; font-weight: 600; }
.karvan-room-title { flex: 1 1 auto; color: #FFD700; font-weight: 700; text-align: center;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.karvan-expire { color: #FFDD44; font-size: 12px; white-space: nowrap; }
.karvan-live { display: inline-flex; align-items: center; gap: 6px; color: #FFDD44; font-size: 12px; margin-bottom: 8px; }
.karvan-live .kl-dot { width: 8px; height: 8px; border-radius: 50%; background: #777; }
.karvan-live.on .kl-dot { background: #35d07f; }
.karvan-live.on .kl-txt { color: #35d07f; }
.karvan-msgs { list-style: none; margin: 0; padding: 6px 0; display: flex; flex-direction: column; gap: 6px;
max-height: 58vh; overflow-y: auto; }
.karvan-msg { max-width: 82%; align-self: flex-start; background: #1c1c1c; border: 1px solid #333;
border-radius: 14px; padding: 7px 11px; box-sizing: border-box; }
.karvan-msg-self { align-self: flex-end; background: #2a2412; border-color: #FFD700; }
.karvan-msg-live { border-left: 2px solid #35d07f; }
.karvan-msg .km-from { display: block; font-size: 10px; color: #FFDD44; margin-bottom: 2px; }
.karvan-msg .km-text { color: #FFD700; word-break: break-word; }
.karvan-compose { display: flex; gap: 8px; margin-top: 10px; }
.karvan-compose input[type="text"] { flex: 1 1 auto; min-width: 0; padding: 10px 12px;
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; }
/* Fase 2: invitar a un contacto por feed id */
.karvan-invite { display: flex; gap: 8px; margin-top: 8px; }
.karvan-invite input[type="text"] { flex: 1 1 auto; min-width: 0; padding: 9px 12px;
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; font-family: monospace; font-size: 12px; }
.karvan-invite button { flex: 0 0 auto; padding: 9px 14px; border: 1px solid #FFD700; background: #161616; color: #FFD700; border-radius: 10px; cursor: pointer; }
.karvan-invite-msg { font-size: 12px; margin: 6px 0 0; }
.karvan-invite-msg.ok { color: #35d07f; }
.karvan-invite-msg.err { color: #ff8a8a; }
/* --- Karvan videollamada (Fase 1): media P2P sobre el mismo WebRTC del chat --- */
/* el div{background:#222;padding:20px} de style.css y el div{border} del tema
convierten cada contenedor del panel en una caja: aqui se neutralizan */
.karvan-call, .karvan-videos, .karvan-call-bar, .kc-status {
background-color: transparent; box-shadow: none; border: 0; border-radius: 0; padding: 0; margin: 0;
}
.karvan-call { margin: 10px 0; }
.karvan-videos { display: none; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px; margin-bottom: 8px; }
.karvan-call.active .karvan-videos { display: grid; }
.kc-video { width: 100%; aspect-ratio: 4 / 3; background: #000; border-radius: 10px; border: 1px solid #333; object-fit: cover; display: block; }
.kc-local { transform: scaleX(-1); }
.kc-remote { display: contents; }
.karvan-call-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.kc-btn { min-height: 44px; min-width: 44px; padding: 8px 12px; border-radius: 999px;
border: 1px solid #333; background: #161616; color: #FFD700; line-height: 1; cursor: pointer; }
.kc-btn.on { background: #FFD700; color: #121212; border-color: #FFD700; }
.kc-btn-start { font-weight: 600; }
.kc-btn-stop { background: #241414; border-color: #5a2a2a; }
.kc-status { font-size: 12px; color: #FFDD44; margin-top: 6px; min-height: 14px; }
/* ==========================================================================
Selector de identidades (ajustes)
========================================================================== */
.identity-list { list-style: none; margin: 10px 0; padding: 0; }
.identity-row { display: flex; align-items: center; justify-content: space-between;
gap: 12px; flex-wrap: wrap; padding: 8px 0; border-bottom: 1px solid #2a2a2a; }
.identity-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.identity-name { color: #FFD700; font-weight: 700; }
.identity-feed { color: #FFDD44; font-size: 12px; font-family: monospace; word-break: break-all; }
.identity-warn { color: #ff8a8a; font-size: 12px; }
.identity-active { color: #35d07f; font-size: 13px; font-weight: 600; white-space: nowrap; }

View file

@ -775,76 +775,7 @@ button, input[type="submit"], input[type="button"],
.header .launcher { width: 100%; max-width: 440px; box-sizing: border-box; margin: 4px auto 10px; padding: 0 14px; }
/* holgura bajo la topbar fija */
body { padding-top: 74px; }
/* ==========================================================================
FORK_IA_UX · Módulo KARVAN (mensajes temporales + WebRTC) — chat estilo burbujas
========================================================================== */
.karvan-intro h2 { color: #FFD700; margin: 0 0 6px; }
.karvan-sub { color: #FFDD44; font-size: 13px; }
.karvan-create input[type="text"] { width: 100%; box-sizing: border-box; padding: 10px 12px;
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; }
.karvan-ttl { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 10px 0; }
.karvan-ttl-lbl { color: #FFDD44; font-size: 13px; }
.karvan-ttl-opt { display: inline-flex; align-items: center; gap: 4px; color: #FFD700; font-size: 13px; }
.karvan-create button, .karvan-compose button { padding: 10px 16px; border: 1px solid #FFD700;
border-radius: 10px; background: #FFD700; color: #121212; font-weight: 700; cursor: pointer; }
.karvan-rooms { list-style: none; margin: 8px 0 0; padding: 0; }
.karvan-rooms li { margin: 0 0 8px; }
.karvan-room-item { display: flex; justify-content: space-between; align-items: center; gap: 10px;
padding: 12px 14px; background: #161616; border: 1px solid #333; border-radius: 12px;
color: #FFD700; text-decoration: none; min-height: 48px; box-sizing: border-box; }
.karvan-room-item .kr-title { font-weight: 600; }
.karvan-room-item .kr-meta { color: #FFDD44; font-size: 12px; white-space: nowrap; }
.karvan-empty { color: #FFDD44; }
/* sala */
.karvan-room-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.karvan-back { color: #FFD700; text-decoration: none; font-weight: 600; }
.karvan-room-title { flex: 1 1 auto; color: #FFD700; font-weight: 700; text-align: center;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.karvan-expire { color: #FFDD44; font-size: 12px; white-space: nowrap; }
.karvan-live { display: inline-flex; align-items: center; gap: 6px; color: #FFDD44; font-size: 12px; margin-bottom: 8px; }
.karvan-live .kl-dot { width: 8px; height: 8px; border-radius: 50%; background: #777; }
.karvan-live.on .kl-dot { background: #35d07f; }
.karvan-live.on .kl-txt { color: #35d07f; }
.karvan-msgs { list-style: none; margin: 0; padding: 6px 0; display: flex; flex-direction: column; gap: 6px;
max-height: 58vh; overflow-y: auto; }
.karvan-msg { max-width: 82%; align-self: flex-start; background: #1c1c1c; border: 1px solid #333;
border-radius: 14px; padding: 7px 11px; box-sizing: border-box; }
.karvan-msg-self { align-self: flex-end; background: #2a2412; border-color: #FFD700; }
.karvan-msg-live { border-left: 2px solid #35d07f; } /* recibido al instante por WebRTC (P2P) */
.karvan-msg .km-from { display: block; font-size: 10px; color: #FFDD44; margin-bottom: 2px; }
.karvan-msg .km-text { color: #FFD700; word-break: break-word; }
.karvan-compose { display: flex; gap: 8px; margin-top: 10px; }
.karvan-compose input[type="text"] { flex: 1 1 auto; min-width: 0; padding: 10px 12px;
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; }
/* Fase 2: invitar a un contacto por feed id */
.karvan-invite { display: flex; gap: 8px; margin-top: 8px; }
.karvan-invite input[type="text"] { flex: 1 1 auto; min-width: 0; padding: 9px 12px;
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; font-family: monospace; font-size: 12px; }
.karvan-invite button { flex: 0 0 auto; padding: 9px 14px; border: 1px solid #FFD700; background: #161616; color: #FFD700; border-radius: 10px; cursor: pointer; }
.karvan-invite-msg { font-size: 12px; margin: 6px 0 0; }
.karvan-invite-msg.ok { color: #35d07f; }
.karvan-invite-msg.err { color: #ff8a8a; }
/* --- Karvan videollamada (Fase 1): media P2P sobre el mismo WebRTC del chat --- */
.karvan-call, .karvan-videos, .karvan-call-bar, .kc-status {
background-color: transparent; box-shadow: none; border-radius: 0; padding: 0; margin: 0; /* anular div{#222} */
}
.karvan-call { margin: 10px 0; }
.karvan-videos { display: none; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px; margin-bottom: 8px; }
.karvan-call.active .karvan-videos { display: grid; }
.kc-video { width: 100%; aspect-ratio: 4 / 3; background: #000; border-radius: 10px; border: 1px solid #333; object-fit: cover; display: block; }
.kc-local { transform: scaleX(-1); } /* espejo, como cámara frontal */
.kc-remote { display: contents; } /* los vídeos remotos entran en la misma rejilla */
.karvan-call-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.kc-btn { min-height: 44px; min-width: 44px; padding: 8px 12px; border-radius: 999px;
border: 1px solid #333; background: #161616; color: #FFD700; line-height: 1; cursor: pointer; }
.kc-btn.on { background: #FFD700; color: #121212; border-color: #FFD700; }
.kc-btn-start { font-weight: 600; }
.kc-btn-stop { background: #241414; border-color: #5a2a2a; }
.kc-status { font-size: 12px; color: #FFDD44; margin-top: 6px; min-height: 14px; }
body { padding-top: 74px; } /* espejo { display: contents; }
/* ==========================================================================
1.3.x: upstream trae en mobile.css su propio chrome movil (cabecera fija, cajones

View file

@ -1,6 +1,6 @@
/* Piezas de usabilidad de la rama movil, para los dos skins (Acampada y OasisMobile).
*
* Va la ultima, detras de karvan.css. Los colores salen de las variables de
* Va la ultima, detras del skin. Los colores salen de las variables de
* Acampada (--a26-*) si el tema las define y, si no, de la paleta oscura con oro
* de OasisMobile; asi cada pieza nueva se ve bien en los dos sin duplicarla.
*
@ -31,7 +31,7 @@
/* fichas de filtro, pestañas y botones secundarios: letra del sistema */
.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group, .filters) .filter-btn:not(.create-button),
.main-column .filter-box__button, .main-column .doc-export-actions .filter-btn,
.main-column .update-btn, .main-column .tribe-action-btn, .lang-chip, .kc-btn,
.main-column .update-btn, .main-column .tribe-action-btn, .lang-chip,
.main-column table th, .card-chips-row > span, .pm-exposition-text {
font-family: var(--ux-text) !important;
font-weight: 700 !important;
@ -59,9 +59,7 @@
}
/* el boton de enviar de los formularios: a todo lo ancho, abajo, que se vea que es
el final. Antes unos centrados, otros a la izquierda y otros enteros */
.main-column :is(.vote-form, .karvan-create, .karvan-compose, .karvan-invite) form > button[type="submit"],
.main-column :is(.vote-form, .karvan-create, .karvan-compose, .karvan-invite) > form > button[type="submit"],
el final. Antes unos centrados,
.main-column form[action$="/tribes/create"][method="post"] > button[type="submit"],
.main-column form[action*="/tribes/update/"] > button[type="submit"] {
display: block !important;
@ -69,7 +67,7 @@
margin: 8px 0 0 !important;
}
/* ---------- iconos SVG (lanzador, barra, modulos, Karvan) ---------- */
/* ---------- iconos SVG (lanzador, barra, modulos) ---------- */
.has-svg { display: inline-flex !important; align-items: center; justify-content: center; }
.has-svg svg { width: 100%; height: 100%; display: block; }
.ln-ico.has-svg { padding: 15px !important; }
@ -263,22 +261,6 @@
}
.main-column .lang-pick .lang-chip.is-current, .lang-pick .lang-chip.is-current { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
/* ---------- Karvan: barra de llamada con nombre en cada boton ---------- */
.karvan-call-bar { display: grid !important; grid-template-columns: repeat(4, 1fr); gap: 8px !important; }
.main-column .karvan-call-bar .kc-btn, .karvan-call-bar .kc-btn {
display: flex !important; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
width: 100% !important; min-height: 64px !important; padding: 8px 4px !important; margin: 0 !important;
font-size: 13px !important; text-transform: none !important; box-shadow: none !important;
}
.kc-btn .kc-ico { width: 24px; height: 24px; }
.kc-btn .kc-lbl { line-height: 1.1; }
.main-column .karvan-call-bar .kc-btn-start { background: var(--ux-ok) !important; color: #fff !important; border-color: var(--ux-ok) !important; }
.main-column .karvan-call-bar .kc-btn.on { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
.main-column .karvan-call-bar .kc-btn-stop { color: var(--ux-danger) !important; border-color: var(--ux-danger) !important; }
.kc-status:not(:empty) { margin-top: 10px; padding: 8px 12px; border-radius: 10px; background: var(--ux-soft); font-size: 14px; font-weight: 700; }
.karvan-invite { margin-top: 24px !important; padding-top: 16px !important; border-top: 2px solid var(--ux-line) !important; }
.karvan-invite-lbl { display: block; margin-bottom: 6px; font-weight: 700; }
/* ---------- foco visible para teclado y lectores ---------- */
a:focus-visible, button:focus-visible, summary:focus-visible, label.module-switch:focus-within {
outline: 3px solid var(--ux-accent) !important; outline-offset: 2px;
@ -407,15 +389,7 @@ body .main-content > .main-column { padding-left: 0 !important; padding-right: 0
font-weight: 700 !important;
letter-spacing: .02em !important;
}
.main-column :is(button, input[type="submit"]):not(.create-button):not(.filter-btn):not(.btn-singleview):not(.chat-react-btn):not(.lang-chip):not(.kc-btn):not(.confirm-yes):not(.welcome-skip-btn) {
font-size: 16px !important;
}
.main-column form.welcome-skip-end { display: flex !important; justify-content: center !important; }
/* llamada solo con voz: sin el video propio vacio */
.karvan-call:not(.has-cam) .kc-local { display: none !important; }
.karvan-call:not(.has-cam) .karvan-videos:not(:has(.kc-remote video)) { display: none !important; }
.karvan-call.active .kc-btn-start { opacity: .45; pointer-events: none; }
/* la barra de confirmar borrado con su marco rojo (Acampada quita el borde a los div) */
.main-column div.confirm-bar { border: 2px solid var(--ux-danger) !important; }
.main-column .content-action-confirm[open] > summary { opacity: 1 !important; }

View file

@ -364,19 +364,6 @@ button.tribe-action-btn { background: transparent !important; border: 1px solid
.chat-topic-active { background: #FFF3E0 !important; }
.chat-topic-title { color: #2D2D2D !important; }
.chat-workspace-empty { border-color: #DDD !important; }
/* Karvan: su hoja (karvan.css) esta pensada para fondo oscuro; aqui se traduce
a la paleta clara, igual que este tema hace con el resto de la interfaz */
.karvan-back, .karvan-room-title, .kr-title { color: #B25000 !important; }
.karvan-sub, .karvan-expire, .karvan-live, .kr-meta,
.karvan-ttl-lbl, .karvan-ttl-opt { color: #6B5300 !important; }
.karvan-room-item, .karvan-create input[type="text"] {
background-color: #FFFFFF !important; border: 1px solid #E0E0E0 !important; color: #2C2C2C !important;
}
.karvan-msg { background-color: #FFF8E1 !important; border: 1px solid #E6CE86 !important; }
.karvan-msg-self { background-color: #FFEFC2 !important; border-color: #D9A400 !important; }
.karvan-msg .km-from { color: #8A6D00 !important; }
.karvan-msg .km-text { color: #2C2C2C !important; }
.map-viewer, .map-stage div, .map-hero, .map-toolbar, .map-toolbar div, .map-place-results, .map-markers-list, .map-markers-list div, .map-detail-grid, .map-card-thumb .map-viewer { background: transparent !important; border: 0 !important; }
.map-pick-cell, .map-pick-cell:hover { border: 0 !important; padding: 0 !important; border-radius: 0 !important; box-shadow: none !important; }
.map-pick-cell { background: transparent !important; }

View file

@ -3,7 +3,7 @@
// ficheros oasis_*.js (upstream los reescribe entero en cada release).
//
// Contiene tres grupos:
// 1. Claves que anade la rama: el modulo Karvan, el selector de identidades,
// 1. Claves que anade la rama: el selector de identidades,
// la barra inferior (bb*) y el filtro Personal/Social de la topbar.
// 2. Claves que upstream tenia en 0.9.1 y elimino en 0.9.5, pero que el
// codigo de la rama sigue usando (menuBlogs, spreadHint, chatShareUrl...).
@ -17,7 +17,7 @@
module.exports = {
en: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "Camp",
tribeSectionsMore: "More",
tribeSectionsLabel: "Sections",
@ -48,43 +48,11 @@ module.exports = {
moduleBlockedAction: "Turn on {name}",
modulesOnCount: "{on} of {total} sections on",
modulesSaved: "Saved.",
karvanVoiceOn: "Voice on",
karvanCamOn: "Camera on",
karvanNoCam: "No camera permission: the call goes on with voice only.",
karvanNoMic: "No microphone permission. You can allow it in the phone settings; the chat still works.",
karvanMic: "Mic",
karvanCam: "Camera",
karvanHangUp: "Hang up",
karvanInviteLabel: "Invite someone",
karvanInviteHint: "Paste their @id: it is on their profile, under their name.",
karvanRelayHuman: "Through your node",
karvanP2pHuman: "Direct and encrypted",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Pick how Oasis looks on your phone. It applies as soon as you tap it.",
workflowsDescriptionMobile: "A workflow sets which modules are on. On the phone your skin stays as it is.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "This image could not be cleaned of its metadata (location, camera…), so it has not been uploaded. Save it as JPEG or PNG and try again.",
// Karvan
karvanActive: "Active rooms",
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
karvanCall: "Call",
karvanCreate: "Create ephemeral room",
karvanEphemeral: "Ephemeral rooms",
karvanInvite: "Invite",
karvanInviteBad: "✗ That is not a valid @feed-id.",
karvanInviteFail: "✗ Could not send the invitation.",
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
karvanInviteSent: "✓ Invitation sent to their inbox.",
karvanNone: "No active rooms. Create one — it disappears on its own.",
karvanRelay: "server relay",
karvanRoomName: "Room name (optional)",
karvanRooms: "Rooms",
karvanSay: "Temporary message…",
karvanSend: "Send",
karvanTitle: "Karvan",
karvanTtl: "Self-destruct in:",
modulesKarvanDescription: "Ephemeral rooms: messages live only in memory and self-destruct.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "In use",
identitiesBackup: "Export or import an identity",
@ -127,7 +95,7 @@ module.exports = {
spreadMore: "more"
},
es: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "Acampada",
tribeSectionsMore: "Más",
tribeSectionsLabel: "Secciones",
@ -158,43 +126,11 @@ module.exports = {
moduleBlockedAction: "Activar {name}",
modulesOnCount: "{on} de {total} secciones activas",
modulesSaved: "Guardado.",
karvanVoiceOn: "Voz activa",
karvanCamOn: "Cámara activa",
karvanNoCam: "Sin permiso de cámara: la llamada sigue solo con voz.",
karvanNoMic: "Sin permiso de micrófono. Puedes darlo en los ajustes del móvil; el chat sigue funcionando.",
karvanMic: "Micro",
karvanCam: "Cámara",
karvanHangUp: "Colgar",
karvanInviteLabel: "Invitar a alguien",
karvanInviteHint: "Pega su @id: está en su perfil, debajo del nombre.",
karvanRelayHuman: "A través de tu nodo",
karvanP2pHuman: "Directo y cifrado",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Elige cómo se ve Oasis en tu móvil. Se aplica al tocarlo.",
workflowsDescriptionMobile: "Un workflow fija qué módulos están activos. En el móvil tu skin se queda como está.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "No se han podido quitar los metadatos de esta imagen (ubicación, cámara…), así que no se ha subido. Guárdala como JPEG o PNG y vuelve a intentarlo.",
// Karvan
karvanActive: "Salas activas",
karvanBlurb: "Salas que viven solo en memoria y se destruyen solas.",
karvanCall: "Llamar",
karvanCreate: "Crear sala efímera",
karvanEphemeral: "Salas efímeras",
karvanInvite: "Invitar",
karvanInviteBad: "✗ Eso no es un @feed-id válido.",
karvanInviteFail: "✗ No se ha podido enviar la invitación.",
karvanInvitePlaceholder: "Invitar a un contacto por @feed-id…",
karvanInviteSent: "✓ Invitación enviada a su bandeja.",
karvanNone: "No hay salas activas. Crea una: desaparece sola.",
karvanRelay: "a través del servidor",
karvanRoomName: "Nombre de la sala (opcional)",
karvanRooms: "Salas",
karvanSay: "Mensaje temporal…",
karvanSend: "Enviar",
karvanTitle: "Karvan",
karvanTtl: "Se autodestruye en:",
modulesKarvanDescription: "Salas efímeras: los mensajes viven solo en memoria y se autodestruyen.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "En uso",
identitiesBackup: "Exportar o importar una identidad",
@ -243,7 +179,7 @@ module.exports = {
totalspreads: "Réplicas totales"
},
fr: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "Campement",
tribeSectionsMore: "Plus",
tribeSectionsLabel: "Sections",
@ -274,43 +210,11 @@ module.exports = {
moduleBlockedAction: "Activer {name}",
modulesOnCount: "{on} sections actives sur {total}",
modulesSaved: "Enregistré.",
karvanVoiceOn: "Voix active",
karvanCamOn: "Caméra active",
karvanNoCam: "Pas d'accès à la caméra : l'appel continue en voix seule.",
karvanNoMic: "Pas d'accès au micro. Vous pouvez l'autoriser dans les réglages du téléphone ; le chat fonctionne toujours.",
karvanMic: "Micro",
karvanCam: "Caméra",
karvanHangUp: "Raccrocher",
karvanInviteLabel: "Inviter quelqu'un",
karvanInviteHint: "Collez son @id : il est sur son profil, sous son nom.",
karvanRelayHuman: "Via votre nœud",
karvanP2pHuman: "Direct et chiffré",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Choisis l'apparence d'Oasis sur ton téléphone. Elle s'applique dès que tu touches.",
workflowsDescriptionMobile: "Un workflow fixe les modules actifs. Sur le téléphone, ton thème ne change pas.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Impossible de retirer les métadonnées de cette image (position, appareil…), elle n'a donc pas été envoyée. Enregistrez-la en JPEG ou PNG et réessayez.",
// Karvan
karvanActive: "Salons actifs",
karvanBlurb: "Des salons qui ne vivent qu'en mémoire et se détruisent d'eux-mêmes.",
karvanCall: "Appeler",
karvanCreate: "Créer un salon éphémère",
karvanEphemeral: "Salons éphémères",
karvanInvite: "Inviter",
karvanInviteBad: "✗ Ce n'est pas un @feed-id valide.",
karvanInviteFail: "✗ Impossible d'envoyer l'invitation.",
karvanInvitePlaceholder: "Inviter un contact par @feed-id…",
karvanInviteSent: "✓ Invitation envoyée dans sa boîte de réception.",
karvanNone: "Aucun salon actif. Créez-en un : il disparaîtra tout seul.",
karvanRelay: "relais par le serveur",
karvanRoomName: "Nom du salon (facultatif)",
karvanRooms: "Salons",
karvanSay: "Message temporaire…",
karvanSend: "Envoyer",
karvanTitle: "Karvan",
karvanTtl: "Autodestruction dans :",
modulesKarvanDescription: "Salons éphémères : les messages ne vivent qu'en mémoire et s'autodétruisent.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "En cours d'utilisation",
identitiesBackup: "Exporter ou importer une identité",
@ -353,7 +257,7 @@ module.exports = {
spreadMore: "plus"
},
eu: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "Kanpaldia",
tribeSectionsMore: "Gehiago",
tribeSectionsLabel: "Atalak",
@ -384,43 +288,11 @@ module.exports = {
moduleBlockedAction: "Aktibatu {name}",
modulesOnCount: "{total} ataletatik {on} aktibo",
modulesSaved: "Gordeta.",
karvanVoiceOn: "Ahotsa piztuta",
karvanCamOn: "Kamera piztuta",
karvanNoCam: "Kamerarako baimenik ez: deiak ahotsarekin bakarrik jarraitzen du.",
karvanNoMic: "Mikrofonorako baimenik ez. Mugikorraren ezarpenetan eman dezakezu; txatak funtzionatzen jarraitzen du.",
karvanMic: "Mikroa",
karvanCam: "Kamera",
karvanHangUp: "Eskegi",
karvanInviteLabel: "Norbait gonbidatu",
karvanInviteHint: "Itsatsi bere @id-a: bere profilean dago, izenaren azpian.",
karvanRelayHuman: "Zure nodoaren bidez",
karvanP2pHuman: "Zuzena eta zifratua",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Aukeratu nolakoa den Oasis zure mugikorrean. Ukitu bezain laster aplikatzen da.",
workflowsDescriptionMobile: "Workflow batek zein modulu dauden aktibo finkatzen du. Mugikorrean zure itxura ez da aldatzen.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Ezin izan dira irudi honen metadatuak kendu (kokapena, kamera…), beraz ez da igo. Gorde JPEG edo PNG gisa eta saiatu berriro.",
// Karvan
karvanActive: "Gela aktiboak",
karvanBlurb: "Memorian bakarrik bizi diren eta beren kabuz suntsitzen diren gelak.",
karvanCall: "Deitu",
karvanCreate: "Sortu gela iragankorra",
karvanEphemeral: "Gela iragankorrak",
karvanInvite: "Gonbidatu",
karvanInviteBad: "✗ Hori ez da @feed-id baliozkoa.",
karvanInviteFail: "✗ Ezin izan da gonbidapena bidali.",
karvanInvitePlaceholder: "Gonbidatu kontaktu bat @feed-id bidez…",
karvanInviteSent: "✓ Gonbidapena bere sarrera-ontzira bidali da.",
karvanNone: "Ez dago gela aktiborik. Sortu bat: bere kabuz desagertuko da.",
karvanRelay: "zerbitzariaren bidez",
karvanRoomName: "Gelaren izena (aukerakoa)",
karvanRooms: "Gelak",
karvanSay: "Aldi baterako mezua…",
karvanSend: "Bidali",
karvanTitle: "Karvan",
karvanTtl: "Bere burua suntsituko du:",
modulesKarvanDescription: "Gela iragankorrak: mezuak memorian bakarrik bizi dira eta beren burua suntsitzen dute.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "Erabiltzen",
identitiesBackup: "Esportatu edo inportatu nortasun bat",
@ -463,7 +335,7 @@ module.exports = {
spreadMore: "gehiago"
},
de: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "Camp",
tribeSectionsMore: "Mehr",
tribeSectionsLabel: "Bereiche",
@ -494,43 +366,11 @@ module.exports = {
moduleBlockedAction: "{name} einschalten",
modulesOnCount: "{on} von {total} Bereichen aktiv",
modulesSaved: "Gespeichert.",
karvanVoiceOn: "Stimme an",
karvanCamOn: "Kamera an",
karvanNoCam: "Keine Kameraberechtigung: der Anruf läuft nur mit Stimme weiter.",
karvanNoMic: "Keine Mikrofonberechtigung. Du kannst sie in den Telefoneinstellungen erlauben; der Chat funktioniert weiter.",
karvanMic: "Mikro",
karvanCam: "Kamera",
karvanHangUp: "Auflegen",
karvanInviteLabel: "Jemanden einladen",
karvanInviteHint: "Füge die @id ein: sie steht im Profil, unter dem Namen.",
karvanRelayHuman: "Über deinen Knoten",
karvanP2pHuman: "Direkt und verschlüsselt",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Wähle, wie Oasis auf deinem Handy aussieht. Es wird beim Antippen übernommen.",
workflowsDescriptionMobile: "Ein Workflow legt fest, welche Module aktiv sind. Auf dem Handy bleibt dein Skin, wie er ist.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Die Metadaten dieses Bildes (Standort, Kamera…) konnten nicht entfernt werden, daher wurde es nicht hochgeladen. Speichere es als JPEG oder PNG und versuche es erneut.",
// Karvan
karvanActive: "Aktive Räume",
karvanBlurb: "Räume, die nur im Speicher leben und sich von selbst zerstören.",
karvanCall: "Anrufen",
karvanCreate: "Flüchtigen Raum erstellen",
karvanEphemeral: "Flüchtige Räume",
karvanInvite: "Einladen",
karvanInviteBad: "✗ Das ist keine gültige @feed-id.",
karvanInviteFail: "✗ Einladung konnte nicht gesendet werden.",
karvanInvitePlaceholder: "Kontakt per @feed-id einladen…",
karvanInviteSent: "✓ Einladung an den Posteingang gesendet.",
karvanNone: "Keine aktiven Räume. Erstelle einen — er verschwindet von selbst.",
karvanRelay: "über den Server",
karvanRoomName: "Raumname (optional)",
karvanRooms: "Räume",
karvanSay: "Temporäre Nachricht…",
karvanSend: "Senden",
karvanTitle: "Karvan",
karvanTtl: "Selbstzerstörung in:",
modulesKarvanDescription: "Flüchtige Räume: Nachrichten leben nur im Speicher und zerstören sich selbst.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "In Benutzung",
identitiesBackup: "Identität exportieren oder importieren",
@ -573,7 +413,7 @@ module.exports = {
spreadMore: "mehr"
},
it: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "Accampata",
tribeSectionsMore: "Altro",
tribeSectionsLabel: "Sezioni",
@ -604,43 +444,11 @@ module.exports = {
moduleBlockedAction: "Attiva {name}",
modulesOnCount: "{on} sezioni attive su {total}",
modulesSaved: "Salvato.",
karvanVoiceOn: "Voce attiva",
karvanCamOn: "Fotocamera attiva",
karvanNoCam: "Nessun permesso per la fotocamera: la chiamata continua solo in voce.",
karvanNoMic: "Nessun permesso per il microfono. Puoi darlo nelle impostazioni del telefono; la chat continua a funzionare.",
karvanMic: "Micro",
karvanCam: "Fotocamera",
karvanHangUp: "Riaggancia",
karvanInviteLabel: "Invita qualcuno",
karvanInviteHint: "Incolla il suo @id: è nel suo profilo, sotto il nome.",
karvanRelayHuman: "Tramite il tuo nodo",
karvanP2pHuman: "Diretto e cifrato",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Scegli come appare Oasis sul tuo telefono. Si applica appena lo tocchi.",
workflowsDescriptionMobile: "Un workflow stabilisce quali moduli sono attivi. Sul telefono la tua skin resta com'è.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Non è stato possibile rimuovere i metadati di questa immagine (posizione, fotocamera…), quindi non è stata caricata. Salvala come JPEG o PNG e riprova.",
// Karvan
karvanActive: "Stanze attive",
karvanBlurb: "Stanze che vivono solo in memoria e si distruggono da sole.",
karvanCall: "Chiama",
karvanCreate: "Crea stanza effimera",
karvanEphemeral: "Stanze effimere",
karvanInvite: "Invita",
karvanInviteBad: "✗ Non è un @feed-id valido.",
karvanInviteFail: "✗ Impossibile inviare l'invito.",
karvanInvitePlaceholder: "Invita un contatto con @feed-id…",
karvanInviteSent: "✓ Invito inviato alla sua casella.",
karvanNone: "Nessuna stanza attiva. Creane una: sparisce da sola.",
karvanRelay: "tramite il server",
karvanRoomName: "Nome della stanza (facoltativo)",
karvanRooms: "Stanze",
karvanSay: "Messaggio temporaneo…",
karvanSend: "Invia",
karvanTitle: "Karvan",
karvanTtl: "Autodistruzione tra:",
modulesKarvanDescription: "Stanze effimere: i messaggi vivono solo in memoria e si autodistruggono.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "In uso",
identitiesBackup: "Esporta o importa un'identità",
@ -683,7 +491,7 @@ module.exports = {
spreadMore: "altro"
},
pt: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "Acampada",
tribeSectionsMore: "Mais",
tribeSectionsLabel: "Secções",
@ -714,43 +522,11 @@ module.exports = {
moduleBlockedAction: "Ativar {name}",
modulesOnCount: "{on} de {total} secções ativas",
modulesSaved: "Guardado.",
karvanVoiceOn: "Voz ativa",
karvanCamOn: "Câmara ativa",
karvanNoCam: "Sem permissão de câmara: a chamada continua só com voz.",
karvanNoMic: "Sem permissão de microfone. Podes dá-la nas definições do telemóvel; o chat continua a funcionar.",
karvanMic: "Micro",
karvanCam: "Câmara",
karvanHangUp: "Desligar",
karvanInviteLabel: "Convidar alguém",
karvanInviteHint: "Cola o @id da pessoa: está no perfil, por baixo do nome.",
karvanRelayHuman: "Através do teu nó",
karvanP2pHuman: "Direto e cifrado",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Escolhe como o Oasis aparece no teu telemóvel. Aplica-se ao tocar.",
workflowsDescriptionMobile: "Um workflow define que módulos estão ativos. No telemóvel o teu tema fica como está.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Não foi possível remover os metadados desta imagem (localização, câmara…), por isso não foi enviada. Guarda-a como JPEG ou PNG e tenta de novo.",
// Karvan
karvanActive: "Salas ativas",
karvanBlurb: "Salas que vivem apenas em memória e destroem-se sozinhas.",
karvanCall: "Chamar",
karvanCreate: "Criar sala efémera",
karvanEphemeral: "Salas efémeras",
karvanInvite: "Convidar",
karvanInviteBad: "✗ Isso não é um @feed-id válido.",
karvanInviteFail: "✗ Não foi possível enviar o convite.",
karvanInvitePlaceholder: "Convidar um contacto por @feed-id…",
karvanInviteSent: "✓ Convite enviado para a caixa de entrada.",
karvanNone: "Não há salas ativas. Cria uma: desaparece sozinha.",
karvanRelay: "através do servidor",
karvanRoomName: "Nome da sala (opcional)",
karvanRooms: "Salas",
karvanSay: "Mensagem temporária…",
karvanSend: "Enviar",
karvanTitle: "Karvan",
karvanTtl: "Autodestrói-se em:",
modulesKarvanDescription: "Salas efémeras: as mensagens vivem apenas em memória e autodestroem-se.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "Em uso",
identitiesBackup: "Exportar ou importar uma identidade",
@ -793,7 +569,7 @@ module.exports = {
spreadMore: "mais"
},
zh: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "营地",
tribeSectionsMore: "更多",
tribeSectionsLabel: "板块",
@ -824,43 +600,11 @@ module.exports = {
moduleBlockedAction: "开启 {name}",
modulesOnCount: "已开启 {on}/{total} 个板块",
modulesSaved: "已保存。",
karvanVoiceOn: "语音已开",
karvanCamOn: "摄像头已开",
karvanNoCam: "没有摄像头权限:通话仅以语音继续。",
karvanNoMic: "没有麦克风权限。可以在手机设置中允许;聊天仍可使用。",
karvanMic: "麦克风",
karvanCam: "摄像头",
karvanHangUp: "挂断",
karvanInviteLabel: "邀请某人",
karvanInviteHint: "粘贴对方的 @id:在其个人资料中,名字下方。",
karvanRelayHuman: "通过你的节点",
karvanP2pHuman: "直连且加密",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "选择 Oasis 在手机上的外观。点一下就会生效。",
workflowsDescriptionMobile: "工作流决定启用哪些模块。在手机上,你的外观保持不变。",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "无法清除这张图片的元数据(位置、相机等),因此没有上传。请另存为 JPEG 或 PNG 后重试。",
// Karvan
karvanActive: "活跃房间",
karvanBlurb: "只存在于内存中、会自行销毁的房间。",
karvanCall: "通话",
karvanCreate: "创建临时房间",
karvanEphemeral: "临时房间",
karvanInvite: "邀请",
karvanInviteBad: "✗ 这不是有效的 @feed-id。",
karvanInviteFail: "✗ 无法发送邀请。",
karvanInvitePlaceholder: "用 @feed-id 邀请联系人…",
karvanInviteSent: "✓ 邀请已发送至对方收件箱。",
karvanNone: "没有活跃房间。创建一个吧——它会自行消失。",
karvanRelay: "经服务器中转",
karvanRoomName: "房间名称(可选)",
karvanRooms: "房间",
karvanSay: "临时消息…",
karvanSend: "发送",
karvanTitle: "Karvan",
karvanTtl: "自动销毁于:",
modulesKarvanDescription: "临时房间:消息只存在于内存中,并会自动销毁。",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "使用中",
identitiesBackup: "导出或导入身份",
@ -903,7 +647,7 @@ module.exports = {
spreadMore: "更多"
},
ar: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "مخيم",
tribeSectionsMore: "المزيد",
tribeSectionsLabel: "الأقسام",
@ -934,43 +678,11 @@ module.exports = {
moduleBlockedAction: "تفعيل {name}",
modulesOnCount: "{on} من {total} أقسام مفعّلة",
modulesSaved: "تم الحفظ.",
karvanVoiceOn: "الصوت مفعّل",
karvanCamOn: "الكاميرا مفعّلة",
karvanNoCam: "لا إذن للكاميرا: تستمر المكالمة بالصوت فقط.",
karvanNoMic: "لا إذن للميكروفون. يمكنك السماح به من إعدادات الهاتف؛ الدردشة تعمل.",
karvanMic: "ميكروفون",
karvanCam: "كاميرا",
karvanHangUp: "إنهاء",
karvanInviteLabel: "ادعُ شخصًا",
karvanInviteHint: "الصق معرّفه @id: تجده في ملفه تحت اسمه.",
karvanRelayHuman: "عبر عقدتك",
karvanP2pHuman: "مباشر ومشفّر",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "اختر مظهر Oasis على هاتفك. يُطبَّق بمجرد اللمس.",
workflowsDescriptionMobile: "يحدد سير العمل الوحدات المفعّلة. على الهاتف يبقى مظهرك كما هو.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "تعذّر إزالة البيانات الوصفية من هذه الصورة (الموقع، الكاميرا…)، لذلك لم تُرفع. احفظها بصيغة JPEG أو PNG وحاول مجددًا.",
// Karvan
karvanActive: "الغرف النشطة",
karvanBlurb: "غرف تعيش في الذاكرة فقط وتدمّر نفسها بنفسها.",
karvanCall: "اتصال",
karvanCreate: "إنشاء غرفة مؤقتة",
karvanEphemeral: "غرف مؤقتة",
karvanInvite: "دعوة",
karvanInviteBad: "✗ هذا ليس @feed-id صالحًا.",
karvanInviteFail: "✗ تعذّر إرسال الدعوة.",
karvanInvitePlaceholder: "ادعُ جهة اتصال عبر @feed-id…",
karvanInviteSent: "✓ تم إرسال الدعوة إلى صندوق الوارد.",
karvanNone: "لا توجد غرف نشطة. أنشئ واحدة، وستختفي وحدها.",
karvanRelay: "عبر الخادم",
karvanRoomName: "اسم الغرفة (اختياري)",
karvanRooms: "الغرف",
karvanSay: "رسالة مؤقتة…",
karvanSend: "إرسال",
karvanTitle: "Karvan",
karvanTtl: "التدمير الذاتي خلال:",
modulesKarvanDescription: "غرف مؤقتة: الرسائل تعيش في الذاكرة فقط وتدمّر نفسها.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "قيد الاستخدام",
identitiesBackup: "تصدير هوية أو استيرادها",
@ -1013,7 +725,7 @@ module.exports = {
spreadMore: "المزيد"
},
hi: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "शिविर",
tribeSectionsMore: "और",
tribeSectionsLabel: "खंड",
@ -1044,43 +756,11 @@ module.exports = {
moduleBlockedAction: "{name} चालू करें",
modulesOnCount: "{total} में से {on} खंड चालू",
modulesSaved: "सहेजा गया।",
karvanVoiceOn: "आवाज़ चालू",
karvanCamOn: "कैमरा चालू",
karvanNoCam: "कैमरे की अनुमति नहीं: कॉल केवल आवाज़ से जारी है।",
karvanNoMic: "माइक्रोफ़ोन की अनुमति नहीं। फ़ोन की सेटिंग में दे सकते हैं; चैट चलती रहेगी।",
karvanMic: "माइक",
karvanCam: "कैमरा",
karvanHangUp: "कॉल काटें",
karvanInviteLabel: "किसी को आमंत्रित करें",
karvanInviteHint: "उनका @id चिपकाएँ: यह उनकी प्रोफ़ाइल में नाम के नीचे है।",
karvanRelayHuman: "आपके नोड के ज़रिए",
karvanP2pHuman: "सीधा और एन्क्रिप्टेड",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "चुनें कि आपके फ़ोन पर Oasis कैसा दिखे। छूते ही लागू हो जाता है।",
workflowsDescriptionMobile: "वर्कफ़्लो तय करता है कि कौन-से मॉड्यूल चालू हैं। फ़ोन पर आपकी स्किन वैसी ही रहती है।",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "इस छवि का मेटाडेटा (स्थान, कैमरा…) हटाया नहीं जा सका, इसलिए इसे अपलोड नहीं किया गया। इसे JPEG या PNG के रूप में सहेजें और फिर कोशिश करें।",
// Karvan
karvanActive: "सक्रिय कक्ष",
karvanBlurb: "ऐसे कक्ष जो केवल मेमोरी में रहते हैं और स्वयं नष्ट हो जाते हैं।",
karvanCall: "कॉल",
karvanCreate: "अस्थायी कक्ष बनाएँ",
karvanEphemeral: "अस्थायी कक्ष",
karvanInvite: "आमंत्रित करें",
karvanInviteBad: "✗ यह मान्य @feed-id नहीं है।",
karvanInviteFail: "✗ आमंत्रण नहीं भेजा जा सका।",
karvanInvitePlaceholder: "@feed-id से संपर्क आमंत्रित करें…",
karvanInviteSent: "✓ आमंत्रण उनके इनबॉक्स में भेजा गया।",
karvanNone: "कोई सक्रिय कक्ष नहीं। एक बनाएँ — यह स्वयं गायब हो जाएगा।",
karvanRelay: "सर्वर के माध्यम से",
karvanRoomName: "कक्ष का नाम (वैकल्पिक)",
karvanRooms: "कक्ष",
karvanSay: "अस्थायी संदेश…",
karvanSend: "भेजें",
karvanTitle: "Karvan",
karvanTtl: "स्वतः नष्ट होगा:",
modulesKarvanDescription: "अस्थायी कक्ष: संदेश केवल मेमोरी में रहते हैं और स्वयं नष्ट हो जाते हैं।",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "उपयोग में",
identitiesBackup: "पहचान निर्यात या आयात करें",
@ -1123,7 +803,7 @@ module.exports = {
spreadMore: "और"
},
ru: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
workflow_acampada: "Лагерь",
tribeSectionsMore: "Ещё",
tribeSectionsLabel: "Разделы",
@ -1154,43 +834,11 @@ module.exports = {
moduleBlockedAction: "Включить {name}",
modulesOnCount: "Включено {on} из {total} разделов",
modulesSaved: "Сохранено.",
karvanVoiceOn: "Голос включён",
karvanCamOn: "Камера включена",
karvanNoCam: "Нет доступа к камере: звонок продолжается только голосом.",
karvanNoMic: "Нет доступа к микрофону. Его можно разрешить в настройках телефона; чат продолжает работать.",
karvanMic: "Микрофон",
karvanCam: "Камера",
karvanHangUp: "Завершить",
karvanInviteLabel: "Пригласить кого-нибудь",
karvanInviteHint: "Вставьте его @id: он в профиле, под именем.",
karvanRelayHuman: "Через ваш узел",
karvanP2pHuman: "Напрямую и зашифровано",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Выберите, как Oasis выглядит на телефоне. Применяется сразу по нажатию.",
workflowsDescriptionMobile: "Рабочий процесс задаёт, какие модули включены. На телефоне ваше оформление не меняется.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Не удалось удалить метаданные этого изображения (местоположение, камера…), поэтому оно не загружено. Сохраните его как JPEG или PNG и попробуйте снова.",
// Karvan
karvanActive: "Активные комнаты",
karvanBlurb: "Комнаты, которые живут только в памяти и уничтожаются сами.",
karvanCall: "Позвонить",
karvanCreate: "Создать эфемерную комнату",
karvanEphemeral: "Эфемерные комнаты",
karvanInvite: "Пригласить",
karvanInviteBad: "✗ Это неверный @feed-id.",
karvanInviteFail: "✗ Не удалось отправить приглашение.",
karvanInvitePlaceholder: "Пригласить контакт по @feed-id…",
karvanInviteSent: "✓ Приглашение отправлено во входящие.",
karvanNone: "Активных комнат нет. Создайте — она исчезнет сама.",
karvanRelay: "через сервер",
karvanRoomName: "Название комнаты (необязательно)",
karvanRooms: "Комнаты",
karvanSay: "Временное сообщение…",
karvanSend: "Отправить",
karvanTitle: "Karvan",
karvanTtl: "Самоуничтожение через:",
modulesKarvanDescription: "Эфемерные комнаты: сообщения живут только в памяти и самоуничтожаются.",
modulesKarvanLabel: "Karvan",
// Identidades
identitiesActive: "Используется",
identitiesBackup: "Экспортировать или импортировать личность",

View file

@ -1,317 +0,0 @@
/* KARVAN client — mensajes temporales + WebRTC (data-channel).
* Capa fiable: relay por servidor (RAM, autodestrucción) con polling.
* Capa turbo: data-channel WebRTC P2P (instantáneo) con "perfect negotiation".
* Todo degrada: si no hay WebRTC o no conecta, el chat sigue por el relay.
* Sin cámara/mic (solo data-channel) => no necesita permisos del wrapper. */
(function () {
"use strict";
var root = document.querySelector(".karvan-room");
if (!root) return;
var roomId = root.getAttribute("data-room");
var selfId = root.getAttribute("data-self") || "";
var peerId = "p" + Math.random().toString(16).slice(2, 12);
var fromLabel = selfId || peerId;
var listEl = document.getElementById("karvan-msgs");
var formEl = document.getElementById("karvan-form");
var textEl = document.getElementById("karvan-text");
var liveTxt = document.getElementById("karvan-live-txt");
var liveWrap = document.getElementById("karvan-live");
var seen = Object.create(null);
var lastSeq = 0;
var lastSig = 0;
var peers = Object.create(null);
// --- Fase 1: videollamada (media sobre el MISMO WebRTC del chat) ---
var callRoot = document.getElementById("karvan-call");
var localVid = document.getElementById("karvan-local");
var remoteEl = document.getElementById("karvan-remote");
var statusEl = document.getElementById("kc-status");
var btnStart = document.getElementById("kc-start");
var btnMic = document.getElementById("kc-mic");
var btnCam = document.getElementById("kc-cam");
var btnStop = document.getElementById("kc-stop");
var localStream = (window.MediaStream ? new MediaStream() : null);
if (localVid && localStream) localVid.srcObject = localStream;
var micActive = false, camActive = false;
function esc(s) { return String(s == null ? "" : s); }
function shortId(id) { id = esc(id || "?"); return id.charAt(0) === "@" ? id.slice(1, 7) : id.slice(0, 6); }
function showMsg(m, live) {
var key = m.mid ? "m" + m.mid : "s" + (m.seq || Math.random());
if (seen[key]) return;
seen[key] = 1;
var li = document.createElement("li");
li.className = "karvan-msg" + (m.from === fromLabel ? " karvan-msg-self" : "") + (live ? " karvan-msg-live" : "");
var f = document.createElement("span"); f.className = "km-from"; f.textContent = shortId(m.from);
var t = document.createElement("span"); t.className = "km-text"; t.textContent = esc(m.text);
li.appendChild(f); li.appendChild(t);
listEl.appendChild(li);
listEl.scrollTop = listEl.scrollHeight;
}
// --- relay por servidor (siempre activo) ---
function pollMsgs() {
fetch("/karvan/" + roomId + "/msgs?since=" + lastSeq, { headers: { Accept: "application/json" } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d || !d.messages) return;
d.messages.forEach(function (m) { if (m.seq > lastSeq) lastSeq = m.seq; showMsg(m, false); });
})
.catch(function () {});
}
function sendMessage(text) {
if (!text) return;
var mid = peerId + "-" + Date.now().toString(36) + Math.random().toString(16).slice(2, 6);
var m = { mid: mid, from: fromLabel, text: text };
showMsg(m, false); // optimista
broadcastDC({ kind: "chat", mid: mid, from: fromLabel, text: text }); // instantáneo a los pares
fetch("/karvan/" + roomId + "/msg", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: fromLabel, text: text, mid: mid })
}).catch(function () {});
}
if (formEl) {
formEl.addEventListener("submit", function (e) {
e.preventDefault();
var v = (textEl.value || "").trim();
if (!v) return;
textEl.value = "";
sendMessage(v);
});
}
// --- señalización (mailbox RAM del servidor) ---
function sendSig(to, payload) {
fetch("/karvan/" + roomId + "/signal", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: peerId, to: to || "", payload: payload })
}).catch(function () {});
}
function announce() { sendSig("", { kind: "hello" }); }
function pollSignals() {
fetch("/karvan/" + roomId + "/signal?for=" + peerId + "&since=" + lastSig, { headers: { Accept: "application/json" } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d) return;
(d.signals || []).forEach(function (s) {
if (s.seq > lastSig) lastSig = s.seq;
onSignal(s.from, s.payload);
});
(d.members || []).forEach(function (mid2) { if (mid2 && mid2 !== peerId) ensurePeer(mid2); });
})
.catch(function () {});
}
// --- WebRTC (data-channel, perfect negotiation) ---
var RTC = window.RTCPeerConnection || window.webkitRTCPeerConnection;
// Sin servidores ICE por defecto: solo candidatos host (LAN y NAT amable).
// Un STUN de terceros aprende la IP publica y el momento de cada llamada, asi que
// no se pone ninguno de fabrica. Si el nodo ofrece TURN propio, /karvan/ice lo da.
var rtcConfig = { iceServers: [] };
function setLive(on) {
if (!liveTxt) return;
liveTxt.textContent = on ? (liveWrap && liveWrap.getAttribute("data-txt-p2p") || "🔒 P2P (WebRTC)") : (liveWrap && liveWrap.getAttribute("data-txt-relay") || "server relay");
if (liveWrap) liveWrap.className = "karvan-live" + (on ? " on" : "");
}
function broadcastDC(obj) {
var s = JSON.stringify(obj);
for (var k in peers) {
var dc = peers[k] && peers[k].dc;
if (dc && dc.readyState === "open") { try { dc.send(s); } catch (e) {} }
}
}
function attachDC(peer, dc) {
peer.dc = dc;
dc.onopen = function () { setLive(true); };
dc.onmessage = function (e) {
try { var m = JSON.parse(e.data); if (m && m.kind === "chat") showMsg(m, true); } catch (x) {}
};
dc.onclose = function () {
var any = false;
for (var k in peers) if (peers[k].dc && peers[k].dc.readyState === "open") any = true;
if (!any) setLive(false);
};
}
function ensurePeer(remoteId) {
if (peers[remoteId] || !RTC) return peers[remoteId];
var polite = peerId > remoteId; // el id mayor es "polite"
var pc = new RTC(rtcConfig);
var peer = { pc: pc, dc: null, polite: polite, makingOffer: false, ignoreOffer: false, vid: null, remoteStream: null };
peers[remoteId] = peer;
if (!polite) { attachDC(peer, pc.createDataChannel("chat")); } // el impolite inicia el canal
pc.ondatachannel = function (e) { attachDC(peer, e.channel); };
// media: publicar los tracks locales que ya tengamos (si la llamada está activa) + recibir los del par
if (localStream) { localStream.getTracks().forEach(function (t) { try { pc.addTrack(t, localStream); } catch (e) {} }); }
pc.ontrack = function (e) { attachRemote(peer, remoteId, e); };
pc.onicecandidate = function (e) { if (e.candidate) sendSig(remoteId, { kind: "ice", candidate: e.candidate }); };
pc.onnegotiationneeded = function () {
peer.makingOffer = true;
Promise.resolve()
.then(function () { return pc.setLocalDescription(); })
.then(function () { sendSig(remoteId, { kind: "desc", desc: pc.localDescription }); })
.catch(function () {})
.then(function () { peer.makingOffer = false; });
};
pc.onconnectionstatechange = function () {
var st = pc.connectionState;
if (st === "failed" || st === "closed") { try { pc.close(); } catch (e) {} removeRemoteVideo(peer); delete peers[remoteId]; }
};
return peer;
}
// --- media / videollamada (Fase 1) ---
function setCallActive(on) { if (callRoot) callRoot.className = "karvan-call" + (on ? " active" : "") + (camActive ? " has-cam" : ""); }
function setStatus(msg) { if (statusEl) statusEl.textContent = msg || ""; }
// textos traducidos: los pone la vista en data-txt-* de #karvan-call
function txt(k, def) { return (callRoot && callRoot.getAttribute("data-txt-" + k)) || def; }
function statusText() { var p = []; if (micActive) p.push(txt("mic", "Voice on")); if (camActive) p.push(txt("cam", "Camera on")); return p.length ? p.join(" · ") : ""; }
function updateBtns() {
if (btnMic) btnMic.className = "kc-btn" + (micActive ? " on" : "");
if (btnCam) btnCam.className = "kc-btn" + (camActive ? " on" : "");
// sin camara no se ensena el video propio: era un recuadro gris con el icono de play
if (callRoot) callRoot.classList.toggle("has-cam", !!camActive);
}
function attachRemote(peer, remoteId, e) {
if (!remoteEl) return;
if (!peer.vid) {
peer.vid = document.createElement("video");
peer.vid.autoplay = true; peer.vid.playsInline = true; peer.vid.className = "kc-video kc-remote-vid";
peer.vid.setAttribute("data-peer", remoteId);
remoteEl.appendChild(peer.vid);
}
if (e.streams && e.streams[0]) peer.vid.srcObject = e.streams[0];
else { if (!peer.remoteStream) peer.remoteStream = new MediaStream(); peer.remoteStream.addTrack(e.track); peer.vid.srcObject = peer.remoteStream; }
setCallActive(true);
}
function removeRemoteVideo(peer) {
if (peer && peer.vid && peer.vid.parentNode) { try { peer.vid.parentNode.removeChild(peer.vid); } catch (_) {} peer.vid = null; }
}
function addLocalTrack(track) {
if (!localStream) return;
localStream.addTrack(track);
for (var k in peers) {
var pc = peers[k].pc, senders = pc.getSenders(), existing = null;
for (var s = 0; s < senders.length; s++) if (senders[s].track && senders[s].track.kind === track.kind) { existing = senders[s]; break; }
if (existing) { try { existing.replaceTrack(track); } catch (_) {} } // swap (cambio de cámara) — sin renegociar
else { try { pc.addTrack(track, localStream); } catch (_) {} } // kind nuevo — dispara onnegotiationneeded
}
}
function removeLocalKind(kind) {
if (!localStream) return;
localStream.getTracks().filter(function (t) { return t.kind === kind; }).forEach(function (t) { t.stop(); localStream.removeTrack(t); });
for (var k in peers) {
var senders = peers[k].pc.getSenders();
for (var s = 0; s < senders.length; s++) if (senders[s].track && senders[s].track.kind === kind) { try { senders[s].replaceTrack(null); } catch (_) {} }
}
}
function onMediaErr(kind, x) {
var name = (x && x.name) || "", msg = (x && x.message) || String(x);
if (name === "NotAllowedError" || /denied|permission/i.test(msg)) setStatus(kind === "video" ? txt("nocam", "No camera permission: the call goes on with voice only.") : txt("nomic", "No microphone permission. You can allow it in the phone settings; the chat still works."));
else if (name === "NotFoundError") setStatus("⚠ No hay " + (kind === "audio" ? "micrófono" : "cámara") + ".");
else if (name === "NotReadableError") setStatus("⚠ Dispositivo ocupado.");
else setStatus("⚠ " + (name || "error") + ": " + msg);
}
function getMedia(kind) {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
var c = (kind === "audio") ? { audio: true, video: false } : { audio: false, video: true };
setStatus((kind === "audio" ? "🎤" : "🎥") + " pidiendo permiso…");
navigator.mediaDevices.getUserMedia(c).then(function (s) {
s.getTracks().forEach(addLocalTrack);
if (kind === "audio") micActive = true; else camActive = true;
setCallActive(true); updateBtns(); setStatus(statusText());
}).catch(function (x) { onMediaErr(kind, x); });
}
// Llamar empieza solo con voz: pedir la camara de entrada hacia que, si se
// negaba, no arrancara nada aunque el micro estuviera permitido. El video se
// anade despues con su boton
function startCall() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
if (micActive) return;
getMedia("audio");
}
function hangUp() {
if (localStream) localStream.getTracks().forEach(function (t) { t.stop(); localStream.removeTrack(t); });
for (var k in peers) { var sn = peers[k].pc.getSenders(); for (var s = 0; s < sn.length; s++) { try { sn[s].replaceTrack(null); } catch (_) {} } }
micActive = false; camActive = false; setCallActive(false); updateBtns(); setStatus("");
}
if (btnStart) btnStart.addEventListener("click", startCall);
if (btnMic) btnMic.addEventListener("click", function () { if (micActive) { removeLocalKind("audio"); micActive = false; updateBtns(); setStatus(statusText()); } else getMedia("audio"); });
if (btnCam) btnCam.addEventListener("click", function () { if (camActive) { removeLocalKind("video"); camActive = false; updateBtns(); setStatus(statusText()); } else getMedia("video"); });
if (btnStop) btnStop.addEventListener("click", hangUp);
function onSignal(fromRemote, payload) {
if (!RTC || !payload) return;
var peer = ensurePeer(fromRemote);
if (!peer) return;
var pc = peer.pc;
if (payload.kind === "desc") {
var desc = payload.desc;
if (!desc) return; // señal malformada (sin desc) — no reventar el lote de sondeo
var collision = desc.type === "offer" && (peer.makingOffer || pc.signalingState !== "stable");
peer.ignoreOffer = !peer.polite && collision;
if (peer.ignoreOffer) return;
Promise.resolve()
.then(function () { return pc.setRemoteDescription(desc); })
.then(function () {
if (desc.type === "offer") {
return pc.setLocalDescription().then(function () {
sendSig(fromRemote, { kind: "desc", desc: pc.localDescription });
});
}
})
.catch(function () {});
} else if (payload.kind === "ice") {
Promise.resolve().then(function () { return pc.addIceCandidate(payload.candidate); }).catch(function () {});
}
}
// --- arranque ---
showMsgInitialSeq();
function showMsgInitialSeq() {
// registrar los mensajes ya renderizados por el servidor para no duplicarlos
var items = listEl ? listEl.querySelectorAll(".karvan-msg") : [];
for (var i = 0; i < items.length; i++) {
var seq = parseInt(items[i].getAttribute("data-seq"), 10) || 0;
var mid = items[i].getAttribute("data-mid") || "";
if (seq > lastSeq) lastSeq = seq;
seen[mid ? "m" + mid : "s" + seq] = 1;
}
}
pollMsgs();
setInterval(pollMsgs, 2000);
if (RTC) {
setLive(false);
// La configuracion ICE tiene que estar resuelta antes de la primera
// RTCPeerConnection: no se puede cambiar de forma fiable una ya creada.
var iceReady = fetch("/karvan/ice", { headers: { accept: "application/json" } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (d && Array.isArray(d.iceServers) && d.iceServers.length) {
rtcConfig = { iceServers: d.iceServers };
if (d.relayOnly) rtcConfig.iceTransportPolicy = "relay";
}
})
.catch(function () {});
var iceTimeout = new Promise(function (res) { setTimeout(res, 2000); });
Promise.race([iceReady, iceTimeout]).then(function () {
announce();
setInterval(announce, 8000);
pollSignals();
setInterval(pollSignals, 2000);
});
}
})();