diff --git a/GUIA_DE_DESARROLLO.md b/GUIA_DE_DESARROLLO.md index a46f338..3ca2aee 100644 --- a/GUIA_DE_DESARROLLO.md +++ b/GUIA_DE_DESARROLLO.md @@ -84,9 +84,8 @@ Aquí se puede hacer lo que haga falta. - `src/views/hive_views.js`: lanzador y barras de arriba y abajo. - `src/views/mobile_icons.js`: los iconos. - `src/backend/fork_routes.js`: nuestro router. Va justo delante del del dev. -- `src/backend/mobile_layer.js`: lo que el móvil necesita del backend (rutas que se - cortan, módulos que no van, avatar y lanzador, retoques del HTML, descargas de la - copia de seguridad). La monta `fork_routes.js`. +- `src/backend/mobile_layer.js`: lo que nuestra interfaz necesita del backend + (avatar, filtro y lanzador de la barra de arriba). La monta `fork_routes.js`. - `src/models/fork_workflows.js`: nuestros workflows. - `src/client/assets/translations/i18n_fork.js`: nuestros textos. - `src/client/assets/images/ux-*.svg`: nuestras imágenes. @@ -156,7 +155,8 @@ nombre es el de la ruta: `/tribes` es `tribes`. ## Cuando sale versión nueva del dev -1. Lo suyo se coge tal cual. +1. Lo suyo se coge tal cual, y para los ficheros con versión de móvil, la de su APK + oficial (`assets/nodejs-project.zip`), no la de su git. 2. En sus vistas se vuelven a poner los enganches. El `grep -n mobileUi` sobre la versión anterior de la rama dice dónde iban. 3. Se repite la medición de `docs/FRONTERA_UPSTREAM.md`: qué ha tocado y si ha @@ -188,19 +188,25 @@ inspeccionar la WebView desde `chrome://inspect`. ## Lo que todavía está mal -Cosas de antes de estas normas que aún no las cumplen, por orden de lo que más -duele: +Para el móvil, la referencia es el `nodejs-project` de la APK oficial del dev, no su +git: él mete cambios de móvil (en `backend.js`, `SSB_server.js`, `phone_module.js`...) +que solo están en la APK. Comparado con eso, la rama cambia 17 ficheros suyos, 561 +líneas (19/10/2026). Por tamaño: -1. En `backend.js` quedan 9 trozos pequeños (21 líneas) que no se pueden sacar desde - fuera: tres expresiones regulares que con el Node 18 de Android tumbaban el - arranque (esto habría que contárselo al dev), no abrir el navegador al arrancar, - no repetir fotos ya limpiadas, el modo chats y la línea que monta nuestro router. - Lo demás ya está en `src/backend/mobile_layer.js`. -2. En `workflows_model.js` queda la logica de skins del movil (`themeOf`, `sameTheme`): - `settings_view.js` coge `currentWorkflow` al cargar y no se puede envolver desde - fuera sin tocar esa vista. Es poco, pero es del dev. -3. `src/server/phone_module.js` escribe el estado de la llamada en - `.phone_state.json` para que la app Android enseñe la notificación (9 líneas). El - módulo del dev no avisa hacia fuera de los cambios de estado; habría que pedírselo. -4. `src/server/SSB_server.js` quita la publicación de la dirección del banco al - arrancar. Hay que mirar por qué se quitó y si sigue haciendo falta. +- `backend/blobHandler.js` (190): limpiar los metadatos de las fotos también en + Android. Es una función nuestra metida en su fichero; habría que proponérsela al dev. +- `configs/oasis-config.json` (87) y `configs/config-manager.js` (18): la config de + serie del móvil (tema Acampada, workflow acampada, barra de abajo). +- `views/main_views.js` (62), `settings_view.js` (34), `welcome_view.js` (11), + `tribes_view.js` (9), `vote_view.js` (7): enganches a `mobile_ui.js` y el orden de + carga de las hojas. Es lo que la guía permite, pero hay que revisarlo en cada versión. +- `main.js` (45): DNS del sistema primero, para respetar la VPN. +- `server/ssb_config.js` (30): varias identidades en el mismo móvil. +- `models/workflows_model.js` (17): la lógica de skins del móvil (`themeOf`, + `sameTheme`). `settings_view.js` coge `currentWorkflow` al cargar y no se puede + envolver desde fuera. +- `styles/mobile.css` (14), `bookmark_view.js` (13), `forum_view.js` (7), + `translations/i18n.js` (5, la mezcla con `i18n_fork.js`): arreglos sueltos de antes. +- `backend/backend.js` (2): la línea que monta nuestro router. + +Cuando el dev saque versión, se repite esta comparación con su APK nueva. diff --git a/docs/FRONTERA_UPSTREAM.md b/docs/FRONTERA_UPSTREAM.md index 9f67da1..e8bde8a 100644 --- a/docs/FRONTERA_UPSTREAM.md +++ b/docs/FRONTERA_UPSTREAM.md @@ -138,6 +138,11 @@ nada, pero son peso muerto y confunden. `acampada` se registran desde `models/fork_workflows.js`. La config de serie del movil coincide con `acampada`. Queda en `workflows_model.js` la logica de skins del movil. +- Correccion (19/10, noche): para el movil la referencia buena es el nodejs-project de + la APK oficial, no el git del dev. Parte de lo que la fase C saco de backend.js era + codigo de movil del propio dev; backend.js vuelve a ser su version de movil mas dos + lineas (montar nuestro router), y modules_view.js su version de movil. Contra la APK + oficial la rama cambia 17 ficheros y 561 lineas; la lista esta en la guia. - Fuera Karvan (19/10): el dev ya trae Telefono y Salas, asi que se quita el modulo propio de salas y llamadas (modelo, vista, JS, CSS, router, pruebas, textos). `Clear-SNH.css` y `modules_view.js` vuelven a ser identicos a los del dev y la linea diff --git a/docs/capturas/esquema-rama.png b/docs/capturas/esquema-rama.png index 8d8269f..6ff6027 100644 Binary files a/docs/capturas/esquema-rama.png and b/docs/capturas/esquema-rama.png differ diff --git a/docs/capturas/generar_portada.py b/docs/capturas/generar_portada.py index c794be4..4973ba7 100644 --- a/docs/capturas/generar_portada.py +++ b/docs/capturas/generar_portada.py @@ -161,7 +161,7 @@ def esquema_rama(): ((400, 210, 720, 400), "PANTALLAS", ["mobile_ui.js y las vistas", "mobile_*.js. En una vista", "del dev, como mucho una", "línea que las llama"], NEON), ((740, 210, 1060, 400), "RUTAS", - ["fork_routes.js y", "mobile_layer.js, delante", "del router del dev: lo que", "no atienden pasa al suyo"], NEON), + ["fork_routes.js, delante", "del router del dev: lo", "que no atiende pasa al", "suyo"], NEON), ((1080, 210, 1340, 400), "ANDROID", ["la app del dev tal cual,", "con nuestro backend", "dentro y paquete", "propio"], NEON), ] @@ -173,7 +173,7 @@ def esquema_rama(): caja(d, (60, 500, 1340, 690), "OASIS DEL DEV: NO SE TOCA", ["backend.js y su middleware · los modelos (lo que se publica y cómo se cuenta) · SSB y su arranque", "las traducciones · style.css y mobile.css · los temas, también OasisMobile.css", - "De 6.400 líneas cambiadas en 43 ficheros suyos (octubre) a 1.045 en 40. Ver GUIA_DE_DESARROLLO.md"], BG) + "Comparado con su APK oficial, la rama cambia 17 ficheros suyos y 561 líneas. Ver GUIA_DE_DESARROLLO.md"], BG) out.save(HERE / "esquema-rama.png", optimize=True) diff --git a/src/backend/backend.js b/src/backend/backend.js index 6374a55..39ef244 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -1644,6 +1644,17 @@ debug("Current configuration: %O", config); debug(`You can save the above to ${defaultConfigFile} to make \ these settings the default. See the readme for details.`); const { saveConfig, getConfig } = require('../configs/config-manager'); +if (process.env.OASIS_MOBILE === '1') { + try { + const cfg = getConfig(); + const forced = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'].filter(m => cfg.modules && cfg.modules[`${m}Mod`] !== 'off'); + let dirty = false; + if (forced.length) { forced.forEach(m => { cfg.modules[`${m}Mod`] = 'off'; }); dirty = true; } + const w = cfg.wallet || {}; + if (w.url === 'http://127.0.0.1:17474' && w.user === 'oasis' && w.pass === 'testpass123') { cfg.wallet = { ...w, url: 'http://127.0.0.1:7474', user: '', pass: '' }; dirty = true; } + if (dirty) saveConfig(cfg); + } catch (_) {} +} const oasisCheckPath = "/.well-known/oasis"; process.on("uncaughtException", function (err) { if (err["code"] === "EADDRINUSE") { @@ -5103,6 +5114,7 @@ router }) .get('/blockexplorer', async (ctx) => { + if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; } const userId = getViewerId(); const query = ctx.query || {}; const search = { @@ -5153,6 +5165,7 @@ router ctx.body = renderBlockchainView(blockchainData, filter, userId, search, { inspect, censusBlocks }); }) .get('/blockexplorer/block/:id', async (ctx) => { + if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; } const userId = getViewerId(); const query = ctx.query || {}; const search = { @@ -8473,8 +8486,8 @@ router const available = { profile: true, ux: true, - workflow: isLoopbackRequest(ctx), - theme: true, + workflow: process.env.OASIS_MOBILE !== '1' && isLoopbackRequest(ctx), + theme: process.env.OASIS_MOBILE !== '1', community: checkMod(ctx, 'invitesMod'), greeting: checkMod(ctx, 'feedMod') }; @@ -11509,6 +11522,40 @@ router try { await forumModel.removeOpenInvite(ctx.params.id); } catch (_) { actionFail(ctx); return; } ctx.redirect(safeReturnTo(ctx, `/forum/${encodeURIComponent(ctx.params.id)}`, ['/forum'])); }) + .get('/backup/keys/export', async (ctx) => { + if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } + if (process.env.OASIS_MOBILE !== '1') { ctx.redirect('/backup'); return; } + const pw = String(ctx.query.password || ''); + if (!pw || pw.length < 32) return ctx.redirect('/backup'); + try { + const { filename, data } = await backupModel.exportKeys({ password: pw }); + ctx.set('Content-Type', 'application/octet-stream'); + ctx.set('Content-Disposition', `attachment; filename="${filename}"`); + ctx.set('Content-Length', String(data.length)); + ctx.body = data; + try { onboardingModel.markStep('backup'); } catch (_) {} + } catch (error) { sendErrorPage(ctx, error.message); } + }) + .get('/backup/export', async (ctx) => { + if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } + if (process.env.OASIS_MOBILE !== '1') { ctx.redirect('/backup'); return; } + if (!checkMod(ctx, 'backupMod')) return ctx.redirect('/modules'); + const b = ctx.query || {}; + const pw = String(b.password || ''); + if (pw.length < 32) return ctx.redirect('/backup'); + const rawMods = b.modules === undefined ? [] : (Array.isArray(b.modules) ? b.modules : [b.modules]); + const outPath = path.join(os.tmpdir(), `oasis-backup-${process.pid}-${Date.now()}.oasisbk`); + try { + const res = await backupModel.createBackup({ scope: b.scope, blobs: b.blobs, modules: rawMods, since: b.since }, pw, outPath); + ctx.set('Content-Type', 'application/octet-stream'); + ctx.set('Content-Disposition', `attachment; filename="${res.filename}"`); + ctx.set('Content-Length', String(res.bytes)); + const stream = fs.createReadStream(outPath); + stream.on('close', () => { try { fs.unlinkSync(outPath); } catch (_) {} }); + ctx.body = stream; + try { onboardingModel.markStep('backup'); } catch (_) {} + } catch (error) { try { fs.unlinkSync(outPath); } catch (_) {} sendErrorPage(ctx, error.message); } + }) .post('/backup/keys/export', koaBody(), async (ctx) => { if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } const pw = ctx.request.body.password; @@ -14601,6 +14648,7 @@ router }) .post("/update", koaBody(), async (ctx) => { if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } + if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/settings'); return; } const exec = require("node:util").promisify(require("node:child_process").exec); const repoRoot = path.resolve(__dirname, '..', '..'); const { stdout, stderr } = await exec("git reset --hard && git pull", { cwd: repoRoot }); @@ -15127,6 +15175,7 @@ router }) .post("/modules/preset", koaBody(), async (ctx) => { if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } + if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/modules'); return; } const ALL_MODULES = workflowsModel.ALL_MODULES; const PRESETS = workflowsModel.PRESETS; const preset = String(ctx.request.body.preset || ''); @@ -15138,10 +15187,14 @@ router ctx.redirect('/modules'); }) .post("/save-modules", koaBody(), async (ctx) => { + const MOBILE_UNAVAILABLE_MODS = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia']; if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } const modules = workflowsModel.ALL_MODULES; const cfg = getConfig(); modules.forEach(mod => cfg.modules[`${mod}Mod`] = ctx.request.body[`${mod}Form`] === 'on' ? 'on' : 'off'); + if (process.env.OASIS_MOBILE === '1') { + MOBILE_UNAVAILABLE_MODS.forEach(mod => cfg.modules[`${mod}Mod`] = 'off'); + } saveConfig(cfg); ctx.redirect(`/modules`); }) @@ -15411,6 +15464,14 @@ const kickRefresh = () => { }; setTimeout(() => { if (!sharedState.getLastRefresh()) kickRefresh(); }, 5000); const middleware = [ + async (ctx, next) => { + await next(); + if (process.env.OASIS_MOBILE !== '1' || typeof ctx.body !== 'string' || !String(ctx.type || '').includes('html')) return; + ctx.body = ctx.body.replace(/]*>/g, (tag) => tag.replace(/\bsrc="(\/blob\/[^"#]+)"/, 'src="$1#t=0.1"')); + const tab = ['/mentions', '/inbox', '/phone', '/data', '/tags', '/search', '/settings'].find((t) => ctx.path === t || ctx.path.startsWith(t + '/')); + const headerEnd = ctx.body.indexOf('
'); + if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(` { if (ctx.method === 'GET' && /^recent$/i.test(String(ctx.query.filter || '')) && ['q', 'search'].some(k => String(ctx.query[k] || '').trim())) ctx.query = { ...ctx.query, filter: ctx.query.filter === 'RECENT' ? 'ALL' : 'all' }; await next(); @@ -15515,8 +15576,7 @@ const middleware = [ await next(); }, // la rama movil: su router y su capa de backend (ver GUIA_DE_DESARROLLO.md) - require('./fork_routes')({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, - about, backupModel, onboardingModel }), + require('./fork_routes')({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, about }), routes, async (ctx) => { if (ctx.body != null || (ctx.method !== 'GET' && ctx.method !== 'HEAD')) return; diff --git a/src/backend/fork_routes.js b/src/backend/fork_routes.js index 7a064e8..368ef0c 100644 --- a/src/backend/fork_routes.js +++ b/src/backend/fork_routes.js @@ -3,7 +3,7 @@ // // Va en la cadena justo delante de las rutas del dev (backend.js lo monta con una // sola linea). Lo que no atiende pasa al del dev con next(). Aqui viven: -// - mobile_layer.js: lo que el movil necesita del backend +// - mobile_layer.js: lanzador, filtro y avatar de la barra de arriba // - wrapUpstream: mira lo que contesta el dev (seccion apagada, guardar modulos) // - las rutas propias: tema y Modulos en el movil, barra de abajo, identidades // @@ -51,10 +51,8 @@ const wrapUpstream = async (ctx, next) => { } }; -module.exports = ({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, - about, backupModel, onboardingModel }) => { -const mobileLayer = require('./mobile_layer')({ getConfig, saveConfig, getViewerId, about, backupModel, onboardingModel, - checkMod, isLoopbackRequest, sendErrorPage }); +module.exports = ({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, about }) => { +const mobileLayer = require('./mobile_layer')({ getViewerId, about }); const forkRouter = new koaRouter(); forkRouter diff --git a/src/backend/mobile_layer.js b/src/backend/mobile_layer.js index c89f8ed..3fc3f18 100644 --- a/src/backend/mobile_layer.js +++ b/src/backend/mobile_layer.js @@ -1,49 +1,20 @@ "use strict"; -// Lo que el movil necesita del backend, fuera de backend.js. +// Lo que la interfaz de la rama necesita del backend, fuera de backend.js. // -// Antes eran trozos metidos en backend.js (rutas, middleware y refreshAll), que es -// del dev y cambia en 34 de cada 39 versiones. Ahora va aqui y lo monta el router -// propio (fork_routes.js) delante de las rutas del dev. Nada de esto corre en -// escritorio. Ver GUIA_DE_DESARROLLO.md. +// Prepara lo que leen la barra de arriba y el lanzador de hive_views.js: el filtro +// Personal/Social, si el lanzador se ensena en esta pagina y el avatar propio. Lo +// monta el router propio (fork_routes.js) delante de las rutas del dev. Nada de +// esto corre en escritorio. Ver GUIA_DE_DESARROLLO.md. // -// Hace cuatro cosas: -// - corta en el movil rutas que alli no tienen sentido (blockexplorer, actualizar -// con git, presets de modulos); el cambio de tema lo atiende fork_routes.js -// - mantiene apagados los modulos que no van en el movil, al arrancar y al guardar -// - prepara lo que leen la barra de arriba y el lanzador (avatar, filtro, lanzador) -// - retoca el HTML que sale (videos con fotograma, pestaña actual marcada) -// Y sirve las dos descargas de la copia de seguridad, que en el movil van por GET. +// Lo demas que el movil necesita del backend (modulos que no van, rutas que se +// cortan, retoques del HTML, descargas de la copia de seguridad) lo hace el propio +// dev en la version de movil de su backend.js, la que va dentro de su APK. -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const compose = require('../server/node_modules/koa-compose'); - -const MOBILE_UNAVAILABLE_MODS = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia']; const NULL_IMG = '&' + '0'.repeat(43) + '=.sha256'; -const TAB_PATHS = ['/mentions', '/inbox', '/phone', '/data', '/tags', '/search', '/settings']; const isMobile = () => process.env.OASIS_MOBILE === '1'; const isPage = (p) => !/^\/(assets|image|blob|qr|c)(\/|$)/.test(p); -module.exports = ({ getConfig, saveConfig, getViewerId, about, backupModel, onboardingModel, - checkMod, isLoopbackRequest, sendErrorPage }) => { - // --- al arrancar: modulos que no van en el movil, apagados; y la cartera de - // pruebas que traia la config de serie, fuera - if (isMobile()) { - try { - const cfg = getConfig(); - let dirty = false; - MOBILE_UNAVAILABLE_MODS.forEach(m => { if (cfg.modules && cfg.modules[`${m}Mod`] !== 'off') { cfg.modules[`${m}Mod`] = 'off'; dirty = true; } }); - const w = cfg.wallet || {}; - if (w.url === 'http://127.0.0.1:17474' && w.user === 'oasis' && w.pass === 'testpass123') { - cfg.wallet = { ...w, url: 'http://127.0.0.1:7474', user: '', pass: '' }; - dirty = true; - } - if (dirty) saveConfig(cfg); - } catch (_) {} - } - - // --- avatar propio para el circulo de la barra de arriba (lo lee hive_views) +module.exports = ({ getViewerId, about }) => { let avatarAt = 0; const refreshAvatar = async () => { avatarAt = Date.now(); @@ -56,78 +27,12 @@ module.exports = ({ getConfig, saveConfig, getViewerId, about, backupModel, onbo } catch (_) {} }; - // --- rutas que en el movil se cortan antes de llegar al dev - const guards = async (ctx, next) => { - if (!isMobile()) return next(); - const p = ctx.path; - if (ctx.method === 'GET' && (p === '/blockexplorer' || p.startsWith('/blockexplorer/block/'))) { ctx.redirect('/'); return; } - if (ctx.method === 'POST' && p === '/update') { ctx.redirect('/settings'); return; } - if (ctx.method === 'POST' && p === '/modules/preset') { ctx.redirect('/modules'); return; } + return async (ctx, next) => { + if (!isMobile() || !isPage(ctx.path)) return next(); + global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : ''; + global.__OASIS_SHOW_HIVE__ = (ctx.path === '/activity' || ctx.path === '/'); + if (!avatarAt) await refreshAvatar(); + else if (Date.now() - avatarAt > 60000) refreshAvatar(); return next(); }; - - // --- antes y despues de las rutas del dev - const around = async (ctx, next) => { - if (!isMobile()) return next(); - if (isPage(ctx.path)) { - // filtro de la home (Personal/Social) y lanzador solo en la portada - global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : ''; - global.__OASIS_SHOW_HIVE__ = (ctx.path === '/activity' || ctx.path === '/'); - if (!avatarAt) await refreshAvatar(); - else if (Date.now() - avatarAt > 60000) refreshAvatar(); - } - await next(); - // guardar modulos: los que no van en el movil siguen apagados - if (ctx.method === 'POST' && ctx.path === '/save-modules' && ctx.status >= 300 && ctx.status < 400) { - try { - const cfg = getConfig(); - MOBILE_UNAVAILABLE_MODS.forEach(m => { cfg.modules[`${m}Mod`] = 'off'; }); - saveConfig(cfg); - } catch (_) {} - } - if (typeof ctx.body !== 'string' || !String(ctx.type || '').includes('html')) return; - // los videos sin poster salen en negro en el WebView: se pide el fotograma 0.1 - ctx.body = ctx.body.replace(/]*>/g, (tag) => tag.replace(/\bsrc="(\/blob\/[^"#]+)"/, 'src="$1#t=0.1"')); - // la pestaña de la cabecera que corresponde a esta pagina - const tab = TAB_PATHS.find((t) => ctx.path === t || ctx.path.startsWith(t + '/')); - const headerEnd = ctx.body.indexOf('
'); - if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(` { - if (ctx.method !== 'GET' || (ctx.path !== '/backup/keys/export' && ctx.path !== '/backup/export')) return next(); - if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - if (!isMobile()) { ctx.redirect('/backup'); return; } - const q = ctx.query || {}; - const pw = String(q.password || ''); - if (!pw || pw.length < 32) { ctx.redirect('/backup'); return; } - if (ctx.path === '/backup/keys/export') { - try { - const { filename, data } = await backupModel.exportKeys({ password: pw }); - ctx.set('Content-Type', 'application/octet-stream'); - ctx.set('Content-Disposition', `attachment; filename="${filename}"`); - ctx.set('Content-Length', String(data.length)); - ctx.body = data; - try { onboardingModel.markStep('backup'); } catch (_) {} - } catch (error) { sendErrorPage(ctx, error.message); } - return; - } - if (!checkMod(ctx, 'backupMod')) { ctx.redirect('/modules'); return; } - const rawMods = q.modules === undefined ? [] : (Array.isArray(q.modules) ? q.modules : [q.modules]); - const outPath = path.join(os.tmpdir(), `oasis-backup-${process.pid}-${Date.now()}.oasisbk`); - try { - const res = await backupModel.createBackup({ scope: q.scope, blobs: q.blobs, modules: rawMods, since: q.since }, pw, outPath); - ctx.set('Content-Type', 'application/octet-stream'); - ctx.set('Content-Disposition', `attachment; filename="${res.filename}"`); - ctx.set('Content-Length', String(res.bytes)); - const stream = fs.createReadStream(outPath); - stream.on('close', () => { try { fs.unlinkSync(outPath); } catch (_) {} }); - ctx.body = stream; - try { onboardingModel.markStep('backup'); } catch (_) {} - } catch (error) { try { fs.unlinkSync(outPath); } catch (_) {} sendErrorPage(ctx, error.message); } - }; - - return compose([around, guards, backup]); }; diff --git a/src/views/modules_view.js b/src/views/modules_view.js index 9aeb4a9..c558559 100644 --- a/src/views/modules_view.js +++ b/src/views/modules_view.js @@ -66,13 +66,17 @@ const modulesView = () => { { name: 'wiki', label: i18n.modulesWikiLabel, description: i18n.modulesWikiDescription } ].sort((x, y) => String(x.label || x.name).localeCompare(String(y.label || y.name), undefined, { sensitivity: 'base' })); - const moduleStates = modules.reduce((acc, mod) => { + const MOBILE_UNAVAILABLE = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia']; + const isMobile = process.env.OASIS_MOBILE === '1'; + const visibleModules = isMobile ? modules.filter(mod => !MOBILE_UNAVAILABLE.includes(mod.name)) : modules; + + const moduleStates = visibleModules.reduce((acc, mod) => { acc[`${mod.name}Mod`] = config[`${mod.name}Mod`] === 'on' ? 'on' : 'off'; return acc; }, {}); - const activeModulesCount = modules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length; - const totalModulesCount = modules.length; + const activeModulesCount = visibleModules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length; + const totalModulesCount = visibleModules.length; const disabledModulesCount = totalModulesCount - activeModulesCount; const header = [div({ class: 'tags-header module-header-line' }, @@ -100,7 +104,7 @@ const modulesView = () => { const presetButtons = div({ class: 'preset-group' }, Object.entries(PRESETS).map(([key, mods]) => { const presetLabel = (i18n[`modulesPreset_${key}`] || key).toUpperCase(); - const isActive = modules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on')); + const isActive = visibleModules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on')); return form({ action: "/modules/preset", method: "post", class: "modules-preset-form" }, input({ type: "hidden", name: "preset", value: key }), button({ @@ -115,8 +119,7 @@ const modulesView = () => { i18n.modules, section(header), section( - h2(i18n.modulesPresetTitle || "Common Configurations"), - presetButtons, + ...(isMobile ? [] : [h2(i18n.modulesPresetTitle || "Common Configurations"), presetButtons]), counts, form( { action: "/save-modules", method: "post" }, @@ -127,7 +130,7 @@ const modulesView = () => { td(i18n.modulesModuleDescription), td({ class: 'module-status-cell' }, i18n.modulesModuleStatus) ), - modules.map(mod => + visibleModules.map(mod => tr( td(a({ href: `/${mod.name}` }, mod.label)), td(p(mod.description)),