docs: add detailed change documentation per feature phase

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SITO 2026-04-29 01:22:40 +02:00
parent ae7f0c1a4a
commit 07403d18c4
5 changed files with 717 additions and 84 deletions

79
CONTEXT/00_INDICE.txt Normal file
View file

@ -0,0 +1,79 @@
===============================================================
CONTEXT — INDICE DE DOCUMENTACION
Oasis Mobile | Actualizado: 2026-04-28
===============================================================
Lee esto primero. Cada archivo cubre un area.
--------------------------------------------------------------
FICHEROS DE ESTADO DEL PROYECTO
--------------------------------------------------------------
01_PROYECTO.txt
Que es el proyecto, stack tecnico, estructura de carpetas,
comparacion con el upstream publico, historial de commits.
Lee esto al inicio de cualquier sesion nueva.
03_GIT_ESTADO.txt
Estado de las ramas git, commits, archivos modificados,
problema de conectividad al remoto, comandos de push.
04_ARQUITECTURA.txt
Arquitectura completa: backend Koa, modelos SSB, las 43 vistas
Hyperaxe, sistema CSS, blockchain SSB. Lista todos los archivos.
05_PROXIMOS_PASOS.txt
Tareas pendientes, como correr el proyecto localmente y en
Android Studio, como instalar Claude Code en Android Studio.
--------------------------------------------------------------
FICHEROS DE CAMBIOS (documentan lo que se modifico y por que)
--------------------------------------------------------------
02_QR_IMPLEMENTACION.txt
Resumen de donde estan los QR codes en la app y que libreria
se usa. Indice rapido con numeros de linea.
Para detalle tecnico leer: cambio_qr.txt
cambio_qr.txt
Documentacion tecnica completa de los QR codes.
- Como funciona cada funcion (renderInvitePage, invitesView,
renderInhabitantCard, inhabitantsProfileView)
- Por que el IIFE async en invites_view
- CSS para los QR en cada tema
- Que puede gustar / que puede no gustar del diseño
cambio_invites_trazabilidad.txt
Documentacion del sistema de trazabilidad de invites.
- inviteLog en tribes_model.js (estructura, generateInvite, joinByInvite)
- SSB message type 'pub-invite' en main_models.js
- renderInviteExtra en blockchain_view.js
- Que puede gustar / que puede no gustar
cambio_visual_movil.txt
Documentacion de todos los cambios CSS para movil.
- mobile.css: safe area, layout, filtros horizontales,
botones, QR lightbox, overrides grid inline
- OasisMobile.css: por que se reescribio, que se quito
- main_views.js: viewport-fit=cover y por que importa
- style.css: que se añadio al final
- Que puede gustar / que puede no gustar
--------------------------------------------------------------
NOTA PARA EL DEVELOPER
--------------------------------------------------------------
Los archivos cambio_*.txt estan escritos para que puedas
verificar cada decision mirando el codigo.
Cada seccion indica archivo y numero de linea aproximado.
Cosas que puede valer la pena revisar en persona:
- El IIFE async en invites_view.js (linea 121) — funciona pero es raro
- Los overrides [style*="..."] en mobile.css — fragiles, mejor refactorizar
- El inviteLog guardado en el SSB message del tribe — crece con el tiempo
- viewport-fit=cover en main_views.js — probar en dispositivo real con notch
- El selector div generico que se eliminó en OasisMobile.css — revisar
que ningun componente dependa del fondo oscuro de ese selector
Todo el codigo modificado esta en:
nodejs-project/nodejs-project/src/

View file

@ -1,103 +1,54 @@
=============================================================== ===============================================================
QR CODES — IMPLEMENTACION (commit 54ad8a1) QR CODES — RESUMEN DE IMPLEMENTACION
Ver cambio_qr.txt para la documentacion tecnica detallada
=============================================================== ===============================================================
OBJETIVO: COMMITS: 54ad8a1, b48944e
Añadir QR codes en 3 lugares de la app: DOCUMENTACION COMPLETA: CONTEXT/cambio_qr.txt
1. Invites de tribes (privados y publicos)
2. Invites de pubs SSB
3. Perfil de usuario (ID SSB como QR para compartir y recibir apoyo)
LIBRERIA USADA:
qrcode ^1.5.4 (ya estaba en package.json, usada en wallet_view.js)
Path de require: require('../server/node_modules/qrcode')
Metodo: QRCode.toString(data, { type: 'svg' }) -> devuelve SVG como string
Render: div({ class: 'qr-code', innerHTML: svgString })
-------------------------------------------------------------- --------------------------------------------------------------
1. TRIBE INVITES — tribes_view.js DONDE ESTAN LOS QR EN LA APP
-------------------------------------------------------------- --------------------------------------------------------------
ARCHIVO: nodejs-project/nodejs-project/src/views/tribes_view.js
CAMBIOS: 1. /tribes/generate-invite (POST) -> resultado en renderInvitePage
- Añadido: const QRCode = require('../server/node_modules/qrcode'); -> QR del invite code del tribe (hex string 64 chars)
- renderInvitePage() convertida a async -> Archivo: src/views/tribes_view.js, funcion renderInvitePage (linea 101)
- Genera QR del inviteCode y lo muestra bajo el texto del codigo
FLUJO: 2. /invites (GET) -> caja snh-invite-box
Backend: POST /tribes/generate-invite -> QR del invite code del pub publico de la instancia
-> tribesModel.generateInvite(tribeId) -> devuelve inviteCode (string) -> Archivo: src/views/invites_view.js, funcion invitesView (linea 34)
-> renderInvitePage(inviteCode) -> muestra codigo + QR
Backend ya tenia: ctx.body = await renderInvitePage(...)
ANTES: 3. /inhabitants (GET) -> tarjeta de cada usuario
exports.renderInvitePage = (inviteCode) => { ... } -> QR del SSB ID, solo visible para el propio usuario (isMe)
-> Archivo: src/views/inhabitants_view.js, funcion renderInhabitantCard (linea 83)
DESPUES: 4. /author/:id (GET) -> perfil de cualquier usuario
exports.renderInvitePage = async (inviteCode) => { -> QR del SSB ID, visible para todos los perfiles
const qrSvg = inviteCode ? await QRCode.toString(inviteCode, { type: 'svg' }) : ''; -> Archivo: src/views/inhabitants_view.js, funcion inhabitantsProfileView (linea 233)
// muestra h2 + p con el codigo + div con QR SVG
} 5. /wallet/receive (GET) -> ya existia antes de esta sesion
-> QR de la direccion ECOin
-> Archivo: src/views/wallet_view.js (no modificado en esta sesion)
-------------------------------------------------------------- --------------------------------------------------------------
2. PUB INVITES — invites_view.js LIBRERIA
-------------------------------------------------------------- --------------------------------------------------------------
ARCHIVO: nodejs-project/nodejs-project/src/views/invites_view.js qrcode ^1.5.4 (ya estaba en package.json)
require: const QRCode = require('../server/node_modules/qrcode');
CAMBIOS: uso: await QRCode.toString(string, { type: 'svg' })
- Añadido: const QRCode = require('../server/node_modules/qrcode'); render: div({ class: 'qr-code', innerHTML: svgString })
- invitesView() convertida a async
- El bloque snhInvite (caja con invite code del pub) ahora incluye QR
CONTEXTO:
snhInvite se carga desde: src/configs/snh-invite-code.json
Estructura: { name: string, code: string }
El QR se genera del campo snhInvite.code
RUTA: GET /invites -> ctx.body = await invitesView({})
(backend.js linea 1406 ya tenia await)
-------------------------------------------------------------- --------------------------------------------------------------
3. PERFIL DE USUARIO — inhabitants_view.js CSS CLAVE
-------------------------------------------------------------- --------------------------------------------------------------
ARCHIVO: nodejs-project/nodejs-project/src/views/inhabitants_view.js style.css: .qr-code svg, .qr-code-inline svg, .qr-code-profile svg
mobile.css: idem + .qr-lightbox-overlay (lightbox sin JS)
CAMBIOS: OasisMobile.css: .qr-code svg rect/path (colores tema oscuro)
- Añadido: const QRCode = require('../server/node_modules/qrcode');
- renderInhabitantCard() convertida a async
-> QR solo se genera para el propio usuario (isMe === true)
-> Aparece debajo del ID SSB en la lista de inhabitants
- inhabitantsView() convertida a async
-> Usa Promise.all() para renderizar las cards async
- inhabitantsProfileView() convertida a async
-> QR se genera SIEMPRE (para cualquier perfil visitado)
-> Aparece en el bloque inhabitant-left junto al avatar y karma
RUTAS:
GET /inhabitants -> inhabitantsView (lista de usuarios)
GET /author/:id -> inhabitantsProfileView (perfil individual)
Ambas ya tenian await en backend.js (lineas 1051 y 1066)
ID SSB formato: @<base64>.ed25519
Ejemplo: @abc123...==.ed25519
-------------------------------------------------------------- --------------------------------------------------------------
4. CSS — style.css LO QUE NO SE IMPLEMENTO (pendiente)
-------------------------------------------------------------- --------------------------------------------------------------
ARCHIVO: nodejs-project/nodejs-project/src/client/assets/styles/style.css - Boton "Copiar codigo" junto al QR
- Lectura de QR por camara (requiere Kotlin nativo)
CLASES AÑADIDAS al final del archivo: - QR en vista individual de cada tribe para compartir URL
.qr-code svg -> max-width 200px, centrado - QR para cada pub activo en la tabla de pubs
.qr-code-inline svg -> max-width 140px (en lista inhabitants)
.qr-code-profile svg -> max-width 160px (en perfil)
.user-id-qr -> flex column, para agrupar ID + QR
.user-id-label -> estilo del texto del ID (monospace pequeño)
.invite-code-text -> estilo del codigo de invite (monospace, fondo sutil)
--------------------------------------------------------------
PENDIENTE / POSIBLES MEJORAS:
--------------------------------------------------------------
- Añadir QR tambien en la vista de cada tribe individual (para compartir la URL)
- Permitir escanear QR directamente desde la app (necesita camara nativa)
- Añadir boton "copiar codigo" junto al QR de invite
- QR para pubs individuales en la tabla de pubs activos

View file

@ -0,0 +1,167 @@
===============================================================
CAMBIO TRAZABILIDAD DE INVITES
(commit 0fc10be)
===============================================================
PROBLEMA QUE RESUELVE:
El sistema original no tenia manera de saber:
- Quien genero un invite de tribe
- Cuando se uso un invite
- Quien uso el invite para entrar
Para pubs SSB, la aceptacion de invite no dejaba ningun rastro
en el log SSB propio.
--------------------------------------------------------------
PARTE 1 — TRIBE INVITES: inviteLog en tribes_model.js
--------------------------------------------------------------
ARCHIVO: src/models/tribes_model.js
ESTRUCTURA inviteLog (nueva):
Array de objetos almacenado en el contenido del mensaje SSB del tribe.
Cada entrada:
{
code: string (hex, 64 chars, el mismo que va en tribe.invites[])
generatedBy: string (SSB ID del usuario que genero el invite)
generatedAt: string (ISO 8601)
status: 'pending' | 'used'
usedBy: string | null (SSB ID del usuario que entro con el codigo)
usedAt: string | null (ISO 8601)
}
FUNCION generateInvite (linea 95):
- Genera code con: crypto.randomBytes(INVITE_CODE_BYTES).toString('hex')
- Lee el inviteLog actual del tribe (o [] si no existe)
- Añade nueva entrada con status 'pending', usedBy/usedAt null
- Guarda inviteLog junto al array invites (que ya existia)
- El array invites[] sigue existiendo tal cual para la logica de join
FUNCION joinByInvite (linea 132):
- Busca el tribe que tiene ese code en su invites[]
- Actualiza el inviteLog mapeando: si entry.code === code, marca
status 'used', usedBy = userId, usedAt = now
- Elimina el code de invites[] (el code queda consumido, no reutilizable)
- Guarda members, invites y inviteLog juntos en una sola llamada updateTribeById
FUNCIONES getTribeById / listAll:
Ambas ya exponían los campos del tribe. Se añadio:
inviteLog: Array.isArray(c.inviteLog) ? c.inviteLog : []
Si el tribe fue creado antes de este cambio, inviteLog sera [].
DONDE SE GUARDA ESTO:
El inviteLog se guarda en el contenido del mensaje SSB de actualizacion
del tribe (via updateTribeById que hace ssb.publish({type:'tribe', ...})).
Es decir, el log de invites esta en la blockchain SSB como parte del
historial del tribe. NOTA: si alguien lee el SSB log directamente,
vera el inviteLog serializado en el campo content del mensaje.
--------------------------------------------------------------
PARTE 2 — PUB INVITES: SSB message en main_models.js
--------------------------------------------------------------
ARCHIVO: src/models/main_models.js
PROBLEMA ORIGINAL:
ssb.invite.accept() es una llamada de bajo nivel del protocolo SSB.
Hace el handshake de red pero NO publica ningun mensaje en tu log propio.
No hay manera de saber despues que aceptaste ese pub invite.
SOLUCION:
Inmediatamente despues del handshake exitoso, publicar manualmente
un mensaje SSB en el log propio:
FUNCION acceptInvite (linea 722):
1. Convierte el invite string con toLegacyInvite()
2. Hace ssb.invite.accept(code, callback) — el handshake real
3. Parsea el code con parseRemote() para extraer host y pubId
(parseRemote ya existia en main_models.js, linea 119)
4. Publica: ssb.publish({ type: 'pub-invite', pubHost, pubKey, acceptedAt })
5. Devuelve el resultado del handshake
ESTRUCTURA del mensaje SSB publicado:
{
type: 'pub-invite'
pubHost: string | null (ej: 'pub.miservidor.net:8008')
pubKey: string | null (ej: '@xxxx.ed25519')
acceptedAt: string (ISO 8601)
}
Si parseRemote no puede extraer host o pubId, guarda null.
No rompe el flujo (el await del publish usa resolve() en ambos casos).
DONDE APARECE ESTO:
En el blockchain explorer, en el filtro 'invites',
y en cualquier lectura del SSB log propio.
--------------------------------------------------------------
PARTE 3 — BLOCKCHAIN EXPLORER: blockchain_model.js + blockchain_view.js
--------------------------------------------------------------
blockchain_model.js — filtro 'invites' (linea 209):
filtered = filtered.filter(b => b &&
(b.type === 'pub-invite' ||
(b.type === 'tribe' && Array.isArray(b.content?.inviteLog) && b.content.inviteLog.length > 0))
)
Muestra:
- Todos los mensajes SSB de tipo 'pub-invite'
- Todos los mensajes de tipo 'tribe' que tengan inviteLog con entradas
blockchain_view.js:
- FILTER_LABELS: 'pub-invite': 'PUB INVITE', invites: 'INVITES'
- CAT_BLOCK2: añadido 'invites' a la categoria de filtros secundarios
- TYPE_COLORS: verde (#27ae60) para pub-invite e invites
- getViewDetailsAction: case 'pub-invite' -> '/invites'
renderInviteExtra(block) — funcion nueva (linea 140):
Si block.type === 'pub-invite':
Tabla de 3 filas: PUB HOST | PUB KEY (user-link) | ACCEPTED AT
Si block.type === 'tribe' con inviteLog:
Tabla de 6 columnas:
CODE (primeros 12 chars + ...) |
GENERATED BY (user-link, primeros 16 chars) |
GENERATED AT |
STATUS (badge pending/used) |
USED BY (user-link) |
USED AT
Retorna null en cualquier otro caso.
Se llama dentro del render de cada bloque en la vista del explorer.
--------------------------------------------------------------
QUE PUEDE GUSTAR
--------------------------------------------------------------
- La trazabilidad de tribes es completa: sabes quien genero, cuando,
quien uso, cuando. Todo dentro del propio log SSB.
- Para pubs: al menos queda constancia en tu log propio de que
aceptaste ese pub invite y cuando.
- El inviteLog es retrocompatible: tribes viejas sin inviteLog
simplemente lo tienen como [].
- El code de tribe se elimina de invites[] al usarse (single-use).
- parseRemote ya existia y es la misma funcion que usa otro codigo
del modelo, no hay logica duplicada.
- Formato ISO 8601 para fechas, legible y ordenable.
--------------------------------------------------------------
QUE PUEDE NO GUSTAR / PUNTOS DE DISCUSION
--------------------------------------------------------------
1. El inviteLog se guarda en el mensaje SSB del tribe.
Esto significa que cada vez que alguien genera o usa un invite,
se publica un nuevo mensaje SSB de tipo 'tribe' con el inviteLog actualizado.
Si hay muchos invites, el historial de mensajes de la tribe crece.
No es un problema grave pero es algo a tener en cuenta.
2. El pub-invite SSB message lo publica DESPUES del handshake, no antes.
Si el proceso cae justo entre el handshake y el publish, el handshake
ocurre pero el log no se actualiza. Es una ventana de inconsistencia pequeña
pero existe.
3. El inviteLog esta en el contenido del tribe message, no en un tipo
de mensaje separado. Si alguien actualiza la tribe por otra razon
(cambio de descripcion, imagen...) y no incluye el inviteLog,
se perderia. Habria que revisar que updateTribeById siempre hace merge
de todos los campos del tribe, no solo los que se pasan.
4. El CODE en la tabla del blockchain se trunca a 12 chars.
Es solo visual. El code completo sigue en el dato y en invites[].
5. No hay manera de revocar un invite pendiente desde la UI.
Solo se "consumen" al usarse. Habria que anadir una accion de revocacion.

186
CONTEXT/cambio_qr.txt Normal file
View file

@ -0,0 +1,186 @@
===============================================================
CAMBIO QR — DOCUMENTACION TECNICA
(commit 54ad8a1)
===============================================================
OBJETIVO:
Añadir QR codes en tres puntos de la app para facilitar
el uso en movil: escanear en vez de copiar strings largos.
LIBRERIA:
qrcode ^1.5.4
Ya estaba en package.json (usada previamente en wallet_view.js).
Path de require dentro de las vistas:
const QRCode = require('../server/node_modules/qrcode');
Uso: QRCode.toString(string, { type: 'svg' }) -> devuelve SVG como string
Render: div({ class: 'qr-code', innerHTML: svgString })
innerHTML es un atributo que hyperaxe inyecta directamente como HTML crudo.
--------------------------------------------------------------
1. TRIBE INVITES — tribes_view.js
--------------------------------------------------------------
FUNCION MODIFICADA: renderInvitePage (linea 101)
ANTES:
exports.renderInvitePage = (inviteCode) => { ... }
DESPUES:
exports.renderInvitePage = async (inviteCode) => {
const qrSvg = inviteCode
? await QRCode.toString(inviteCode, { type: 'svg' })
: '';
// ... div con el codigo + div con innerHTML: qrSvg
}
PORQUE NO REQUIRIO CAMBIOS EN BACKEND:
backend.js ya tenia: ctx.body = await renderInvitePage(...)
El await ya estaba, hacer la funcion async no rompio nada.
QUE SE MUESTRA:
- Titulo h2 con el nombre de la tribe
- p con el invite code en texto
- SVG del QR debajo del codigo (clase: 'div-center qr-code')
- Si inviteCode es null/undefined -> qrSvg = '' -> el div QR no se renderiza
--------------------------------------------------------------
2. PUB INVITES — invites_view.js
--------------------------------------------------------------
FUNCION MODIFICADA: invitesView (linea 34)
La funcion padre invitesView ya era async (porque hace otras ops async).
El bloque del QR usa un IIFE async para poder hacer await dentro
de una expresion inline:
await (async () => {
if (!snhInvite) return null;
const qrSvg = snhInvite.code
? await QRCode.toString(snhInvite.code, { type: 'svg' })
: '';
return div({ class: 'snh-invite-box' },
h3({ class: 'snh-invite-name' }, snhInvite.name),
span({ class: 'snh-invite-code' }, snhInvite.code),
qrSvg ? div({ class: 'div-center qr-code', innerHTML: qrSvg }) : null
);
})()
POR QUE EL IIFE:
La estructura de hyperaxe es funcional (cada elemento es un argumento
pasado al elemento padre). No puedes poner un await suelto en medio
de una lista de argumentos. El IIFE resuelve eso sin reestructurar
toda la funcion.
ALTERNATIVA MAS LIMPIA (si se quisiera refactorizar):
Extraer la caja snhInvite a una funcion separada async
y llamarla antes de construir el arbol.
El IIFE funciona, pero es menos legible para alguien que no lo conozca.
DATO DEL CONTEXTO:
snhInvite viene de: src/configs/snh-invite-code.json
Formato: { name: string, code: string }
Es el invite code del pub publico de la instancia.
--------------------------------------------------------------
3. PERFIL DE USUARIO — inhabitants_view.js
--------------------------------------------------------------
TRES FUNCIONES MODIFICADAS:
--- renderInhabitantCard (linea 83) ---
async (user, filter, currentUserId)
QR SOLO se genera si isMe === true (el usuario viendo su propio perfil).
Razon: evitar generar 50 QRs en la lista de inhabitants.
const isMe = user.id === currentUserId;
const qrSvg = isMe && user.id
? await QRCode.toString(user.id, { type: 'svg' })
: '';
El QR aparece dentro de 'div.user-id-qr', junto al enlace user-link.
--- inhabitantsView (linea 177) ---
Debia hacerse async porque llama a renderInhabitantCard (que es async).
Usa Promise.all para no hacer await uno por uno:
await Promise.all(inhabitants.map(user =>
renderInhabitantCard(user, filter, currentUserId)
))
IMPORTANTE: Solo genera QR para isMe, asi que en la practica
solo hay 1 await real de QRCode.toString() en toda la lista.
El Promise.all es correcto igualmente para el futuro.
--- inhabitantsProfileView (linea 233) ---
QR se genera SIEMPRE (para cualquier perfil, no solo el propio).
Razon: la pagina de perfil individual es de una sola persona,
tiene sentido mostrar su QR para que otros lo escaneen y lo sigan.
const profileId = payload?.id || id || viewedId;
const qrSvg = profileId
? await QRCode.toString(profileId, { type: 'svg' })
: '';
Aparece en 'div.inhabitant-left' junto al avatar.
--------------------------------------------------------------
CSS PARA LOS QR
--------------------------------------------------------------
En style.css (al final del archivo):
.qr-code svg -> max-width: 200px, display block, centrado
.qr-code-inline svg -> max-width: 120px (en lista inhabitants, mas pequeno)
.qr-code-profile svg -> max-width: 150px (en perfil individual)
.user-id-qr -> flex column, align-items center
.invite-code-text -> font monospace, fondo sutil
En mobile.css:
.qr-code svg -> max-width: 180px
.qr-code-inline svg -> max-width: 120px
.qr-code-profile svg -> max-width: 150px
.qr-lightbox-overlay -> lightbox CSS puro (sin JS), para ampliar QR
.qr-lightbox-anchor -> ancla que activa el :target del overlay
En OasisMobile.css (tema oscuro):
.qr-code svg rect -> fill: #1A1A1A (fondo del QR oscuro)
.qr-code svg path -> fill: #FFD700 (puntos del QR en dorado)
--------------------------------------------------------------
QUE PUEDE GUSTAR
--------------------------------------------------------------
- No rompe nada: el backend ya tenia await en todas las rutas afectadas
- Libreria qrcode ya existia en el proyecto (no se añadio dependencia nueva)
- QR solo donde tiene sentido: no hay QRs innecesarios en cada elemento
- En lista de inhabitants, QR solo para el propio usuario = sin overhead
- SVG directo (no imagen base64 ni PNG), escala perfecto en cualquier pantalla
- El QR del perfil es el SSB ID completo (@xxx.ed25519), scaneable con
cualquier cliente SSB para seguir al usuario
- Lightbox CSS puro para ampliar QR en movil sin JS adicional
- Tema oscuro: colores del QR adaptados (#1A1A1A fondo, #FFD700 puntos)
--------------------------------------------------------------
QUE PUEDE NO GUSTAR / PUNTOS DE DISCUSION
--------------------------------------------------------------
1. El IIFE async en invites_view.js es un patron poco comun.
Funciona, pero si alguien lee el codigo por primera vez le puede
parecer raro. Alternativa: extraer a funcion separada.
2. El QR en inhabitantsProfileView se genera para TODOS los perfiles.
Si se visita el perfil de un usuario con un SSB ID muy largo,
el QR es pequeno y puede ser dificil de escanear. Considerar
mostrar solo para isMe si se quiere ser mas conservador.
3. El innerHTML con SVG crudo: hyperaxe lo inyecta tal cual.
Si el SVG tuviera algun caracter extraño o fallo de la libreria qrcode,
podria romper el HTML. En la practica qrcode es fiable, pero
es una inyeccion directa a tener en cuenta.
4. No hay boton "Copiar codigo" junto al QR.
En movil es util para pegar el invite en otro lado.
Pendiente de implementar.
5. El QR del tribe invite code es un string hex aleatorio (32 bytes = 64 chars).
No es una URL ni tiene deep link. Solo sirve para copiar/pegar en la app,
no para escanear con una app de QR generica de terceros.
El del SSB ID si que es estandar y reconocible.
6. No se ha implementado lectura de QR (solo generacion).
Leer QR desde la camara requiere acceso nativo Android.
Esto esta fuera del scope del Node.js embebido, habria que
hacerlo desde el wrapper Kotlin y pasar el resultado al servidor.

View file

@ -0,0 +1,250 @@
===============================================================
CAMBIO VISUAL MOVIL — CSS Y LAYOUT
(commit b48944e)
===============================================================
OBJETIVO:
Mejorar la experiencia en pantallas moviles (<=768px).
El proyecto estaba diseñado principalmente para escritorio.
Los cambios afectan tres archivos CSS.
--------------------------------------------------------------
ARCHIVO 1 — mobile.css (reescrito completo, ~480 lineas)
--------------------------------------------------------------
RUTA: src/client/assets/styles/mobile.css
Este archivo se aplica solo en pantallas moviles via media attribute.
Antes tenia estilos dispersos y faltaban muchas cosas.
Se reescribio completamente.
SAFE AREA (notch / home indicator Android e iOS):
html { -webkit-text-size-adjust: 100%; }
body { padding-bottom: env(safe-area-inset-bottom); }
.header {
padding-top: env(safe-area-inset-top);
padding-left/right: env(safe-area-inset-left/right);
}
footer {
padding-bottom: calc(12px + env(safe-area-inset-bottom));
}
Estas variables CSS solo tienen valor si el meta viewport tiene
viewport-fit=cover (ver cambio en main_views.js mas abajo).
FONT SIZE BASE:
body { font-size: 14px; }
Cabeceras: h1 1.25em, h2 1.1em, h3 1em
Textos pequeños (.small, .time, .created-at): 0.78rem
LAYOUT:
.main-content -> flex column (sidebars debajo del contenido)
.sidebar-left -> order: 1
.sidebar-right -> order: 2
.main-column -> order: 3
(el contenido principal queda arriba, sidebars debajo)
Todos los anchos al 100%, min-width: 0 para evitar overflow.
HEADER:
.header -> flex row wrap, padding minimo (4px 2px)
.top-bar-left, .top-bar-right -> display: contents
(los hijos directos participan en el flex del header)
nav ul li a -> padding 5px, font-size 0.82rem
BOTONES Y TOUCH TARGETS:
min-height: 44px (minimo recomendado por Apple HIG / Material)
font-size: 15px
:active { opacity: 0.75; transform: scale(0.97); } -> feedback tactil
INPUTS:
width: 100%, font-size: 16px
(16px evita el zoom automatico en iOS al enfocar un input)
FILTROS (scroll horizontal):
.mode-buttons, .filter-group, .filters, .tribe-mode-buttons:
flex-direction: row
flex-wrap: nowrap
overflow-x: auto
scrollbar-width: none (oculta la scrollbar en Firefox)
::-webkit-scrollbar { display: none } (oculta en Chrome/WebKit)
Cada form dentro: flex-shrink: 0 (no se comprimen)
Los botones de filtro: white-space: nowrap, padding 8px 14px
ANTES: los filtros aparecian en columna vertical, ocupando
media pantalla. DESPUES: fila horizontal scrollable invisible.
IMAGENES DE POSTS:
.post img, .feed-card img, .post-image:
max-height: 60vh (no ocupan toda la pantalla)
width: auto, max-width: 100%
border-radius: 8px
FOTOS DE USUARIO (circulares):
.inhabitant-photo, .inhabitant-photo-details:
width: 160px, height: 160px
border-radius: 50%
object-fit: cover
margin: 0 auto
GALERIA:
.gallery -> grid 2 columnas (1fr 1fr)
TRIBES:
.tribe-card-image -> height: 160px, object-fit: cover
.tribe-section-nav -> overflow-x auto, nowrap (scroll horizontal)
.tribe-section-btn -> font-size 11px, white-space nowrap
.tribe-details -> flex column
.tribe-inhabitants-grid -> 2 columnas
.tribe-media-grid -> 2 columnas
.tribe-content-grid / .tribe-overview-grid -> 1 columna
FORUM:
.forum-card -> flex column
.forum-comment -> margin-left: 0 (no hay espacio para sangria en movil)
.comment-body-row -> flex column
TABLAS:
.feed-row, table -> overflow-x auto (scroll horizontal si es ancha)
QR LIGHTBOX (CSS puro, sin JS):
.qr-lightbox-anchor -> display block, width fit-content
.qr-lightbox-overlay -> display: none por defecto
.qr-lightbox-anchor:target ~ .qr-lightbox-overlay -> display: flex
El overlay es position: fixed, fondo negro 88% opacidad, z-index 9999
El SVG dentro: max-width min(85vw, 85vh)
NOTA: este lightbox usa el selector CSS :target, que se activa
cuando el hash de la URL coincide con el id del elemento.
No requiere JavaScript. Para que funcione, el HTML debe tener
el ancla con href="#id" y el overlay con id="id".
OVERRIDES DE GRID INLINE:
Hay vistas del proyecto que ponen grid-template-columns directamente
en el atributo style del HTML. Para sobreescribir en movil:
[style*="grid-template-columns: repeat(6"] { grid-template-columns: 1fr }
[style*="grid-template-columns: repeat(3"] { grid-template-columns: 1fr }
[style*="width:50%"] { width: 100% }
--------------------------------------------------------------
ARCHIVO 2 — OasisMobile.css (reescrito, solo theming)
--------------------------------------------------------------
RUTA: src/client/assets/themes/OasisMobile.css
Antes tenia el CSS de mobile.css entero copiado dentro + el theming.
Era ~700 lineas con mucha duplicacion y conflictos.
Se reescribio dejando SOLO el theming de colores.
PALETA DEL TEMA:
body: background #121212, color #FFD700
header/footer: background #1F1F1F
sidebars: background #1A1A1A, border #333
inputs/select/textarea: background #333, color #FFD700, border #555
buttons: background #444, color #FFD700
links: color #FFD700, hover #FFDD44
tables: background #222, th #333, even rows #2A2A2A
SELECTORES ESPECIFICOS (en vez de div generico):
Antes habia: div { background-color: #1A1A1A; border: 1px solid #333; }
Eso pintaba absolutamente TODOS los divs de la pagina de oscuro,
rompiendo layouts con divs transparentes o con otro fondo.
Se reemplazo por:
.block, .post, .card, .tribe-card, .inhabitant-card,
.action-container, .feed-card, .block-single,
.invite-page, .wallet-section, .pubs-section,
.invites-tribes, .snh-invite-box, .invite-log-block
SCROLLBAR:
::-webkit-scrollbar -> width/height 6px
thumb: #555, border-radius 10px
track: #1A1A1A
user-link (badge de usuario):
background #FFA500, color #000, bold
hover: background #FFD700
active: background #cc8400, scale(0.97)
focus: background #007B9F (accesibilidad)
overflow hidden + text-overflow ellipsis (para IDs largos)
QR en tema oscuro:
.qr-code svg rect -> fill #1A1A1A
.qr-code svg path -> fill #FFD700
(por defecto la libreria genera QR con fondo blanco y puntos negros)
--------------------------------------------------------------
ARCHIVO 3 — main_views.js (una linea cambiada)
--------------------------------------------------------------
RUTA: src/views/main_views.js
ANTES:
meta({ name: 'viewport', content: toAttributes({
width: 'device-width',
'initial-scale': 1
}) })
DESPUES:
meta({ name: 'viewport', content:
'width=device-width, initial-scale=1, viewport-fit=cover'
})
POR QUE IMPORTA viewport-fit=cover:
Sin esta directiva, env(safe-area-inset-*) siempre devuelve 0.
Es decir, los paddings de safe area del mobile.css no tienen efecto
a menos que el viewport tenga viewport-fit=cover.
Solo afecta a dispositivos con notch, agujero de camara o barra de
gestos de Android. En dispositivos normales no cambia nada visible.
--------------------------------------------------------------
ARCHIVO 4 — style.css (añadidos al final)
--------------------------------------------------------------
RUTA: src/client/assets/styles/style.css
Se añadieron estilos al FINAL del archivo (no se modifico lo existente):
- Fotos circulares (border-radius 50%, object-fit cover)
- .invite-page: flex column, align-items center, max-width 500px
- .user-id-qr: flex column, align-items center, gap 8px
- .invite-code-text: font-family monospace, fondo sutil, padding
- .invite-log-block: padding, border-left 3px solido
- .invite-status--pending: color naranja
- .invite-status--used: color verde
- Transiciones en botones: 0.15s ease
--------------------------------------------------------------
QUE PUEDE GUSTAR
--------------------------------------------------------------
- Filtros de scroll horizontal: gran mejora UX en movil. Los filtros
ya no ocupan media pantalla.
- Safe area funciona correctamente con viewport-fit=cover.
- OasisMobile.css limpio: ya no sobreescribe todos los divs del DOM.
- Touch targets de 44px: botones faciles de pulsar con el dedo.
- Font-size 16px en inputs: evita el zoom automatico de Safari iOS.
- Fotos circulares consistentes en toda la app.
- El lightbox QR no necesita JavaScript adicional.
- Los cambios son aditivos en style.css (no se toco lo anterior).
--------------------------------------------------------------
QUE PUEDE NO GUSTAR / PUNTOS DE DISCUSION
--------------------------------------------------------------
1. El orden de los sidebars (left=1, main=3, right=2) puede parecer
raro. La logica es: sidebar izquierda con navegacion primero,
luego sidebar derecha con info secundaria, luego el feed principal.
Es discutible, podria ser: left=2, main=1, right=3.
2. display:contents en .top-bar-left y .top-bar-right:
Hace que el div desaparezca visualmente y sus hijos participen
en el flex del padre. Es un truco para no reescribir el HTML.
Tiene soporte amplio en browsers modernos pero puede ser confuso
al depurar el layout con las devtools.
3. Los overrides de grid inline ([style*="grid-template-columns: repeat(6"])
son fragiles: dependen de que el string del style en el HTML sea
exactamente ese. Si hay un espacio de mas o menos, no aplica.
La solucion correcta seria eliminar los estilos inline del HTML
y moverlos a clases CSS, pero eso requiere tocar muchas vistas.
4. El header en movil con flex-wrap puede quedar diferente segun
cuantos elementos de navegacion tenga cada pagina. No hay una
maqueta fija, el header se adapta al contenido.
5. El cambio de viewport-fit=cover afecta a todas las paginas de la app.
En dispositivos con notch grande podria mostrar contenido bajo
el notch si alguna pagina no tiene los paddings de safe area bien
configurados. Hay que verificar en dispositivo real.