diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 992d326..96b2723 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -51,6 +51,9 @@ jobs:
- name: Generar iconos (desde app-icon.png)
run: npm run icons
+ - name: Probar el cliente
+ run: npm test
+
- name: Construir, firmar y publicar
uses: tauri-apps/tauri-action@v0
env:
@@ -63,6 +66,18 @@ jobs:
tagName: ${{ github.ref_type == 'tag' && github.ref_name || 'v__VERSION__' }}
releaseName: "Labstream OS Desktop __VERSION__"
releaseBody: "Instaladores de escritorio de Labstream OS.\n\n- Windows: el archivo `.exe` (NSIS).\n- macOS: el archivo `.dmg` universal (Intel + Apple Silicon).\n\nActualización automática activada: las apps ya instaladas se actualizan solas al publicar una versión nueva."
- releaseDraft: false
+ releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
+
+ activar:
+ needs: publish
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Publicar solo cuando ambos instaladores estan completos
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ VERSION=$(node -p "require('./package.json').version")
+ gh release edit "v$VERSION" --draft=false --latest
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 64a0aea..98d75c2 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -44,6 +44,9 @@ jobs:
- name: Instalar dependencias
run: npm install
+ - name: Probar el script del cliente
+ run: npm test
+
# build.rs de Tauri valida que existan los iconos que declara tauri.conf.json.
- name: Generar iconos (desde app-icon.png)
run: npm run icons
diff --git a/README.md b/README.md
index f11276c..aa1f555 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,28 @@ pestañas son webviews hijos `tab-*`. Shell ↔ Rust se hablan por eventos `ls-*
El servidor objetivo se configura en `src-tauri/src/lib.rs` → constante `SERVER_URL`.
+### Portadas autenticadas (1.14.1)
+
+Las imagenes conservan su URL HTTPS y se cargan con la sesion del webview, igual
+que en el navegador. Ya no se reescriben a `lsthumb://`: el descargador Rust no
+comparte las cookies y fallaba con miniaturas sin token o con token vencido.
+Tampoco se modifica el DOM de las imagenes durante la hidratacion.
+
+Se usa la cache HTTP con los ETag y Cache-Control del servidor. Una respuesta
+304 reutiliza los bytes descargados; no implica fabricar otro fotograma. El
+cajon antiguo no se borra, pero no se usa para las nuevas peticiones de imagenes.
+No cambia el almacenamiento de originales, la cache HLS de LabTem ni los datos offline.
+
+Google Docs, Sheets, Drive y el inicio de sesion de Google se abren en el navegador
+del sistema. Google no admite OAuth en webviews controlados por una app; ademas,
+WKWebView no debe anunciarse como Chrome. Se conserva el agente de usuario nativo
+y no se borran cookies ni sesiones. Las hojas conectadas siguen disponibles como
+tabla dentro del area contable. Al reiniciar se restauran solo pestanas de Labstream.
+
+Pruebas: `npm test`; con Playwright disponible en Node,
+`node tests/images.browser.cjs` verifica sesion, 304, carga dinamica y lienzo.
+La prueba de navegador usa un servidor local y una cookie ficticia, sin datos reales.
+
Salidas:
- **Windows** → instalador `.exe` (NSIS).
diff --git a/package-lock.json b/package-lock.json
index 0d50372..01fd9f8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "labstream-desktop",
- "version": "1.4.0",
+ "version": "1.14.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "labstream-desktop",
- "version": "1.4.0",
+ "version": "1.14.1",
"devDependencies": {
"@tauri-apps/cli": "^2"
}
diff --git a/package.json b/package.json
index 0bf1be8..54c3582 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,10 @@
{
"name": "labstream-desktop",
- "version": "1.14.0",
+ "version": "1.14.1",
"private": true,
"description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.",
"scripts": {
+ "test": "node --test tests/images.test.cjs",
"icons": "tauri icon app-icon.png",
"dev": "npm run icons && tauri dev",
"build": "npm run icons && tauri build",
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index f926608..f6ed9e6 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -2366,7 +2366,7 @@ dependencies = [
[[package]]
name = "labstream-desktop"
-version = "1.14.0"
+version = "1.14.1"
dependencies = [
"active-win-pos-rs",
"device_query",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index f1ce527..3dfd2ab 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "labstream-desktop"
-version = "1.14.0"
+version = "1.14.1"
description = "Cliente de escritorio de Labstream OS"
authors = ["Labstream SAS"]
edition = "2021"
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 7bcf2be..81031a7 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -54,11 +54,7 @@ use tauri_plugin_opener::OpenerExt;
const SERVER_URL: &str = "https://os.labstreamsas.com";
-// Los documentos del contador viven en Google (pestaña Contable). Se abren como PESTANAS
-// de esta app, no en el navegador externo: asi el WebView guarda la sesion de Google del
-// usuario (cookies persistentes) y no hay que iniciar sesion cada vez. La lista es cerrada
-// a proposito: solo hosts propios de Google (docs/drive/sheets y accounts para el login).
-// Todo lo demas sigue yendo al navegador del sistema.
+// Google requiere el navegador del sistema para autenticar y editar con compatibilidad completa.
const GOOGLE_HOSTS: &[&str] = &[
"docs.google.com",
"drive.google.com",
@@ -67,29 +63,29 @@ const GOOGLE_HOSTS: &[&str] = &[
"myaccount.google.com",
];
-// Cadena de navegador de escritorio. Sin esto, WebView2 se anuncia como control embebido y
-// Google Sheets/Docs se quedan en blanco. Se declara por plataforma porque Google tambien
-// mira la coherencia entre el sistema declarado y el resto de la cadena.
-#[cfg(target_os = "windows")]
-const UA_ESCRITORIO: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
-#[cfg(target_os = "macos")]
-const UA_ESCRITORIO: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
-#[cfg(not(any(target_os = "windows", target_os = "macos")))]
-const UA_ESCRITORIO: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
-
fn is_google_host(host: &str) -> bool {
let h = host.to_ascii_lowercase();
GOOGLE_HOSTS.iter().any(|d| h == *d || h.ends_with(&format!(".{}", d)))
}
fn es_pestana_interna(target: &Url, server: &Url) -> bool {
- if target.origin() == server.origin() {
- return true;
+ target.origin() == server.origin()
+}
+
+#[cfg(test)]
+mod navigation_tests {
+ use super::*;
+
+ #[test]
+ fn only_labstream_is_an_embedded_tab() {
+ let server = Url::parse(SERVER_URL).unwrap();
+ assert!(es_pestana_interna(&server.join("/revisiones/1").unwrap(), &server));
+ for url in ["https://docs.google.com/document/d/1/edit", "https://accounts.google.com", "https://os.labstreamsas.com.evil.example", "http://os.labstreamsas.com"] {
+ assert!(!es_pestana_interna(&Url::parse(url).unwrap(), &server));
+ }
+ assert!(is_google_host("docs.google.com"));
+ assert!(!is_google_host("docs.google.com.evil.example"));
}
- if target.scheme() != "https" {
- return false;
- }
- target.host_str().map(is_google_host).unwrap_or(false)
}
const TAB_BAR_H: f64 = 40.0; // alto lógico de la barra de pestañas (chrome)
const CHROME: &str = "chrome";
@@ -243,12 +239,7 @@ const INIT_JS_TPL: &str = r#"
// descargar fallaban solo en la app de Windows, y no en Mac ni en el navegador.
// En macOS estas dos líneas son un no-op: allí el script nunca ha corrido en subframes.
try { if (window.top !== window.self) return; } catch (e) { return; }
- // Hosts de Google que la app abre como pestana propia (pestana Contable). En esas
- // paginas solo reportamos titulo a la barra; NO tocamos clics ni window.open para no
- // romper el editor de Google.
- var GOOGLE_RE = /^(?:.*\.)?(?:docs|drive|sheets|accounts|myaccount)\.google\.com$/i;
- var esGoogle = GOOGLE_RE.test(location.hostname);
- if (location.origin !== ORIGIN && !esGoogle) return;
+ if (location.origin !== ORIGIN) return;
if (window.__lsShell) return;
window.__lsShell = true;
@@ -259,8 +250,7 @@ const INIT_JS_TPL: &str = r#"
function abs(h) { try { return new URL(h, location.href); } catch (e) { return null; } }
function isHttp(u) { return !!u && (u.protocol === 'http:' || u.protocol === 'https:'); }
function isApp(u) { return isHttp(u) && u.origin === ORIGIN; }
- function isGoogleUrl(u) { return isHttp(u) && GOOGLE_RE.test(u.hostname); }
- function isInterno(u) { return isApp(u) || isGoogleUrl(u); }
+ function isInterno(u) { return isApp(u); }
function isExt(u) { return isHttp(u) && !isInterno(u); }
function emit(name, payload) { try { T.event.emit(name, payload); } catch (e) {} }
function openExternal(url) {
@@ -269,73 +259,10 @@ const INIT_JS_TPL: &str = r#"
}
function newTab(url) { emit('ls-new-tab', { url: url }); }
- // ── Miniaturas por el CAJÓN de la app (lsthumb://) ──
- // Dentro de la app de escritorio, las miniaturas de la cuadrícula de fotos se piden por el
- // esquema lsthumb:// en vez de directo al servidor. Así las sirve el cajón local (Rust): del
- // disco si ya están —sobreviven a cerrar la app, que es lo que se pedía— y del NAS solo la
- // primera vez. Fuera de la app (navegador normal) este script no corre y todo sigue igual.
- //
- // Se reescribe SOLO la miniatura de 480 px (thumb=1): es la que se repite 500 veces por set y
- // ahoga la cuadrícula. El visor grande (thumb=xl) y la anotación sobre lienzo NO se tocan —
- // cargar la imagen del lienzo por otro esquema lo «ensuciaría» y rompería «guardar el dibujo».
- try {
- function porCajon(src) {
- if (!src || src.lastIndexOf('lsthumb:', 0) === 0) return src; // ya reescrita
- var u = abs(src);
- if (!u || u.origin !== ORIGIN) return src; // solo lo del propio servidor
- if (!/\/api\/files-asset\//.test(u.pathname) || u.searchParams.get('thumb') !== '1') return src;
- return 'lsthumb://c/?u=' + encodeURIComponent(u.href);
- }
- var proto = HTMLImageElement.prototype;
- // 1) La propiedad .src (lo que usa React al asignar la imagen).
- var d = Object.getOwnPropertyDescriptor(proto, 'src');
- if (d && d.get && d.set) {
- Object.defineProperty(proto, 'src', {
- configurable: true, enumerable: d.enumerable,
- get: function () { return d.get.call(this); },
- set: function (v) { d.set.call(this, porCajon(String(v))); },
- });
- }
- // 2) setAttribute('src', …), que no pasa por el setter de la propiedad. Se parchea SOLO en
- // (prototipo de HTMLImageElement), no en todos los elementos.
- var setA = proto.setAttribute;
- proto.setAttribute = function (name, value) {
- if (arguments.length >= 2 && String(name).toLowerCase() === 'src') {
- return setA.call(this, name, porCajon(String(value)));
- }
- return setA.apply(this, arguments);
- };
- // 3) Las que ya vienen en el HTML del servidor (SSR) o llegan luego. porCajon es idempotente
- // (una ya reescrita se devuelve igual), así que barrer no entra en bucle con el observer.
- function barrer(raiz) {
- if (!raiz || !raiz.querySelectorAll) return;
- var imgs = raiz.querySelectorAll('img[src]');
- for (var i = 0; i < imgs.length; i++) {
- var cur = imgs[i].getAttribute('src');
- var nue = porCajon(cur);
- if (nue !== cur) imgs[i].setAttribute('src', nue);
- }
- }
- if (window.MutationObserver) {
- new MutationObserver(function (muts) {
- for (var i = 0; i < muts.length; i++) {
- var m = muts[i];
- if (m.type === 'attributes' && m.target && m.target.tagName === 'IMG') {
- var cur = m.target.getAttribute('src'), nue = porCajon(cur);
- if (nue !== cur) m.target.setAttribute('src', nue);
- }
- for (var j = 0; j < m.addedNodes.length; j++) {
- var n = m.addedNodes[j];
- if (!n || n.nodeType !== 1) continue;
- if (n.tagName === 'IMG') { var c = n.getAttribute('src'), x = porCajon(c); if (x !== c) n.setAttribute('src', x); }
- else barrer(n);
- }
- }
- }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] });
- }
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { barrer(document); });
- else barrer(document);
- } catch (e) { /* si algo falla, las imágenes cargan directo del servidor como siempre */ }
+ // Las imagenes conservan su URL HTTPS y la sesion del webview. El descargador
+ // lsthumb no comparte cookies: falla con miniaturas sin token y omite la
+ // revalidacion de permisos al leer del disco. La cache HTTP respeta los ETag
+ // y Cache-Control del servidor sin modificar src, srcset ni la hidratacion.
// ── ¿La página se VE oscura? ──
// Señales, de la más directa a la más física: la clase `dark` (next-themes) y, si no
@@ -388,7 +315,6 @@ const INIT_JS_TPL: &str = r#"
history[k] = function () { var r = o.apply(this, arguments); setTimeout(report, 0); return r; };
});
addEventListener('popstate', function () { setTimeout(report, 0); });
- if (esGoogle) return; // en Google solo miramos el titulo
// Pulsar la luna cambia clases/estilos sin navegar: se vigilan y