From 07403d18c47ae4468e6bf74aecc05e4ab203cdd2 Mon Sep 17 00:00:00 2001 From: SITO Date: Wed, 29 Apr 2026 01:22:40 +0200 Subject: [PATCH] docs: add detailed change documentation per feature phase Co-Authored-By: Claude Sonnet 4.6 --- CONTEXT/00_INDICE.txt | 79 ++++++++ CONTEXT/02_QR_IMPLEMENTACION.txt | 119 ++++------- CONTEXT/cambio_invites_trazabilidad.txt | 167 ++++++++++++++++ CONTEXT/cambio_qr.txt | 186 ++++++++++++++++++ CONTEXT/cambio_visual_movil.txt | 250 ++++++++++++++++++++++++ 5 files changed, 717 insertions(+), 84 deletions(-) create mode 100644 CONTEXT/00_INDICE.txt create mode 100644 CONTEXT/cambio_invites_trazabilidad.txt create mode 100644 CONTEXT/cambio_qr.txt create mode 100644 CONTEXT/cambio_visual_movil.txt diff --git a/CONTEXT/00_INDICE.txt b/CONTEXT/00_INDICE.txt new file mode 100644 index 00000000..d9a85128 --- /dev/null +++ b/CONTEXT/00_INDICE.txt @@ -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/ diff --git a/CONTEXT/02_QR_IMPLEMENTACION.txt b/CONTEXT/02_QR_IMPLEMENTACION.txt index 66be0baf..15162bc3 100644 --- a/CONTEXT/02_QR_IMPLEMENTACION.txt +++ b/CONTEXT/02_QR_IMPLEMENTACION.txt @@ -1,103 +1,54 @@ =============================================================== - QR CODES — IMPLEMENTACION (commit 54ad8a1) + QR CODES — RESUMEN DE IMPLEMENTACION + Ver cambio_qr.txt para la documentacion tecnica detallada =============================================================== -OBJETIVO: - Añadir QR codes en 3 lugares de la app: - 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 }) +COMMITS: 54ad8a1, b48944e +DOCUMENTACION COMPLETA: CONTEXT/cambio_qr.txt -------------------------------------------------------------- -1. TRIBE INVITES — tribes_view.js +DONDE ESTAN LOS QR EN LA APP -------------------------------------------------------------- -ARCHIVO: nodejs-project/nodejs-project/src/views/tribes_view.js -CAMBIOS: - - Añadido: const QRCode = require('../server/node_modules/qrcode'); - - renderInvitePage() convertida a async - - Genera QR del inviteCode y lo muestra bajo el texto del codigo + 1. /tribes/generate-invite (POST) -> resultado en renderInvitePage + -> QR del invite code del tribe (hex string 64 chars) + -> Archivo: src/views/tribes_view.js, funcion renderInvitePage (linea 101) -FLUJO: - Backend: POST /tribes/generate-invite - -> tribesModel.generateInvite(tribeId) -> devuelve inviteCode (string) - -> renderInvitePage(inviteCode) -> muestra codigo + QR - Backend ya tenia: ctx.body = await renderInvitePage(...) + 2. /invites (GET) -> caja snh-invite-box + -> QR del invite code del pub publico de la instancia + -> Archivo: src/views/invites_view.js, funcion invitesView (linea 34) -ANTES: - exports.renderInvitePage = (inviteCode) => { ... } + 3. /inhabitants (GET) -> tarjeta de cada usuario + -> QR del SSB ID, solo visible para el propio usuario (isMe) + -> Archivo: src/views/inhabitants_view.js, funcion renderInhabitantCard (linea 83) -DESPUES: - exports.renderInvitePage = async (inviteCode) => { - const qrSvg = inviteCode ? await QRCode.toString(inviteCode, { type: 'svg' }) : ''; - // muestra h2 + p con el codigo + div con QR SVG - } + 4. /author/:id (GET) -> perfil de cualquier usuario + -> QR del SSB ID, visible para todos los perfiles + -> Archivo: src/views/inhabitants_view.js, funcion inhabitantsProfileView (linea 233) + + 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 - -CAMBIOS: - - Añadido: const QRCode = require('../server/node_modules/qrcode'); - - 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) + qrcode ^1.5.4 (ya estaba en package.json) + require: const QRCode = require('../server/node_modules/qrcode'); + uso: await QRCode.toString(string, { type: 'svg' }) + render: div({ class: 'qr-code', innerHTML: svgString }) -------------------------------------------------------------- -3. PERFIL DE USUARIO — inhabitants_view.js +CSS CLAVE -------------------------------------------------------------- -ARCHIVO: nodejs-project/nodejs-project/src/views/inhabitants_view.js - -CAMBIOS: - - 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: @.ed25519 -Ejemplo: @abc123...==.ed25519 + style.css: .qr-code svg, .qr-code-inline svg, .qr-code-profile svg + mobile.css: idem + .qr-lightbox-overlay (lightbox sin JS) + OasisMobile.css: .qr-code svg rect/path (colores tema oscuro) -------------------------------------------------------------- -4. CSS — style.css +LO QUE NO SE IMPLEMENTO (pendiente) -------------------------------------------------------------- -ARCHIVO: nodejs-project/nodejs-project/src/client/assets/styles/style.css - -CLASES AÑADIDAS al final del archivo: - .qr-code svg -> max-width 200px, centrado - .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 + - Boton "Copiar codigo" junto al QR + - Lectura de QR por camara (requiere Kotlin nativo) + - QR en vista individual de cada tribe para compartir URL + - QR para cada pub activo en la tabla de pubs diff --git a/CONTEXT/cambio_invites_trazabilidad.txt b/CONTEXT/cambio_invites_trazabilidad.txt new file mode 100644 index 00000000..6d3f22d7 --- /dev/null +++ b/CONTEXT/cambio_invites_trazabilidad.txt @@ -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. diff --git a/CONTEXT/cambio_qr.txt b/CONTEXT/cambio_qr.txt new file mode 100644 index 00000000..b3b3b406 --- /dev/null +++ b/CONTEXT/cambio_qr.txt @@ -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. diff --git a/CONTEXT/cambio_visual_movil.txt b/CONTEXT/cambio_visual_movil.txt new file mode 100644 index 00000000..0ebbd683 --- /dev/null +++ b/CONTEXT/cambio_visual_movil.txt @@ -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.