@glassnote/client 2.4.16 → 2.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INSTALACION.md CHANGED
@@ -63,19 +63,19 @@ donde la app los busca, y el cliente arranca ya conectado.
63
63
  ```sh
64
64
  # Linux / macOS — ojo con el `-s --`, es lo que hace que sh le pase las banderas al script
65
65
  curl -fsSL https://glassnote.intermark.ec/install.sh | sh -s -- \
66
- --org-id 55 --api-key <clave> --device-name "Caja 01" --user-name "Santiago"
66
+ --org-id <id> --api-key <clave> --device-name "Caja 01" --user-name "Santiago"
67
67
  ```
68
68
 
69
69
  ```powershell
70
70
  # Windows — con parámetros: `[scriptblock]` es lo que deja pasárselos al script bajado
71
71
  & ([scriptblock]::Create((irm https://glassnote.intermark.ec/install.ps1))) `
72
- -OrgId 55 -ApiKey <clave> -DeviceName 'Caja 01' -UserName 'Santiago'
72
+ -OrgId <id> -ApiKey <clave> -DeviceName 'Caja 01' -UserName 'Santiago'
73
73
  ```
74
74
 
75
75
  ```powershell
76
76
  # Windows — con variables de entorno: `irm | iex` no deja pasar parámetros, pero el
77
77
  # cliente lee las variables (quedan puestas en esa ventana de PowerShell)
78
- $env:GLASSNOTE_ORG_ID='55'; $env:GLASSNOTE_API_KEY='<clave>'; $env:GLASSNOTE_DEVICE_NAME='Caja 01'; $env:GLASSNOTE_USER_NAME='Santiago'
78
+ $env:GLASSNOTE_ORG_ID='<id>'; $env:GLASSNOTE_API_KEY='<clave>'; $env:GLASSNOTE_DEVICE_NAME='Caja 01'; $env:GLASSNOTE_USER_NAME='Santiago'
79
79
  irm https://glassnote.intermark.ec/install.ps1 | iex
80
80
  ```
81
81
 
@@ -98,6 +98,60 @@ Es el diseño: en GlassNote un dispositivo es un usuario en un equipo. Dos usuar
98
98
  misma máquina son dos dispositivos en el tablero, cada uno con su `device-id`, y cada
99
99
  cliente corre mientras su usuario tiene la sesión abierta.
100
100
 
101
+ Para SCCM/Microsoft Configuration Manager en producción hay dos formas, con el mismo
102
+ paquete offline adentro:
103
+
104
+ | Forma | Cuándo |
105
+ |---|---|
106
+ | **MSI** `glassnote-client-offline.msi` | La recomendada para Configuration Manager e Intune. Se importa como aplicación *Windows Installer*. Lleva el paquete adentro: el equipo no descarga nada. La detección hay que cambiarla a la marca del registro (ver la guía de SCCM). |
107
+ | Bootstrap `install-glassnote-sccm.ps1` | Cuando no se quiere poner 145 MB en el punto de distribución: el script baja el ZIP desde `glassnote.intermark.ec` en cada equipo. |
108
+
109
+ ### El MSI
110
+
111
+ ```text
112
+ https://glassnote.intermark.ec/glassnote-client-offline.msi
113
+ ```
114
+
115
+ Instalar y desinstalar:
116
+
117
+ ```text
118
+ msiexec /i glassnote-client-offline.msi GLASSNOTE_ORG_ID=<id> GLASSNOTE_API_KEY=<clave> /qn
119
+ msiexec /x {ProductCode} /qn
120
+ ```
121
+
122
+ El ProductCode cambia con cada paquete, así que no se escribe a mano: para desinstalar,
123
+ búsquelo en `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` por el nombre
124
+ `GlassNote Client (offline)`. Propiedades: `GLASSNOTE_ORG_ID` y `GLASSNOTE_API_KEY`
125
+ (obligatorias), `GLASSNOTE_API_URL`, `GLASSNOTE_WS_URL`, `GLASSNOTE_DEVICE_NAME`,
126
+ `GLASSNOTE_USER_NAME`. Sin el alta la instalación falla con 1603 y no deja nada.
127
+
128
+ Un paquete nuevo reemplaza al anterior aunque sea la misma versión del cliente, y volver
129
+ a lanzar el mismo paquete repara un equipo donde el producto figura instalado pero el
130
+ cliente no está.
131
+
132
+ El MSI se instala por equipo, pero el cliente es por usuario: la instalación corre como
133
+ SYSTEM, busca al usuario con sesión iniciada e instala en su sesión, espera y devuelve su
134
+ código de salida. **Sin nadie con sesión iniciada no instala** y msiexec devuelve error.
135
+ Desinstalar quita el cliente del usuario, el autoarranque y los archivos del paquete;
136
+ **no toca `%APPDATA%\glassnote`**, así que reinstalar deja el mismo dispositivo.
137
+
138
+ ### El bootstrap
139
+
140
+ ```text
141
+ https://glassnote.intermark.ec/install-glassnote-sccm.ps1
142
+ ```
143
+
144
+ Ese bootstrap descarga:
145
+
146
+ ```text
147
+ https://glassnote.intermark.ec/glassnote-sccm-offline-runtime.zip
148
+ ```
149
+
150
+ El ZIP lo genera GitHub Actions durante el deploy de `glassnote-next` e incluye Node.js
151
+ portable, Electron, cache npm y `glassnote-client-2.4.17.tgz`. Así la instalación inicial
152
+ no depende de `nodejs.org`, GitHub ni `registry.npmjs.org`. El `install.ps1` genérico queda
153
+ para instalaciones manuales, Intune/GPO simples o equipos con salida normal a internet.
154
+
101
155
  Estas herramientas suelen correr los scripts como **SYSTEM**, y un cliente instalado así
102
156
  quedaría en el perfil de SYSTEM, donde nadie lo ve.
103
157
 
@@ -112,7 +166,8 @@ quedaría en el perfil de SYSTEM, donde nadie lo ve.
112
166
  | GPO | script de inicio de sesión en *User Configuration*, no de arranque en *Computer Configuration* |
113
167
 
114
168
  **Cuando no se puede** —*Run Scripts* de SCCM corre siempre como SYSTEM, igual que una
115
- Application con *Install for system*— el instalador lo detecta y no instala en SYSTEM:
169
+ Application con *Install for system*, y un MSI siempre se instala con permisos de
170
+ administrador— el instalador lo detecta y no instala en SYSTEM:
116
171
 
117
172
  1. busca al usuario con sesión iniciada (el dueño de `explorer.exe`);
118
173
  2. deja el instalador en el `%TEMP%` de ese usuario y lo corre con una tarea programada de
@@ -120,9 +175,10 @@ Application con *Install for system*— el instalador lo detecta y no instala en
120
175
  3. espera a que termine, muestra su salida y sale con su código;
121
176
  4. borra la tarea y la carpeta.
122
177
 
123
- El comando es el mismo en los dos casos. Las variables de entorno y los parámetros llegan
124
- a la sesión del usuario; la API key viaja como variable de entorno, nunca en una línea de
125
- comandos, y el script que la lleva se borra al arrancar.
178
+ Lo hacen los tres: `install.ps1`, el paquete offline y el MSI. El comando es el mismo en
179
+ los dos casos. Las variables de entorno y los parámetros llegan a la sesión del usuario; la
180
+ API key viaja como variable de entorno, nunca en una línea de comandos, y el script que la
181
+ lleva se borra al arrancar.
126
182
 
127
183
  ```powershell
128
184
  # el mismo script para todo el parque
@@ -141,9 +197,10 @@ Límites del camino por SYSTEM:
141
197
  *Only when a user is logged on*.
142
198
  - **Instala para el usuario con sesión**, el primero que encuentra. Otro usuario del mismo
143
199
  equipo es otro dispositivo y necesita su propia instalación.
144
- - ***Run Scripts* corta por timeout** (60 s por defecto) y la primera instalación tarda más
145
- (baja electron). Si corta, la instalación sigue en la sesión del usuario, pero SCCM no
146
- ve el resultado. Hay que subir el timeout.
200
+ - ***Run Scripts* corta por timeout** (60 s por defecto). Para SCCM offline suba el
201
+ timeout a unos 1800 s porque baja y extrae un ZIP de alrededor de 150 MB. Si corta,
202
+ revise `C:\ProgramData\GlassNote\install-sccm-bootstrap.log` y
203
+ `C:\ProgramData\GlassNote\install-sccm.log`.
147
204
  - El `out.log` de la instalación queda tomado por el cliente, que hereda esa salida, hasta
148
205
  que se cierra. La carpeta `glassnote-install-*` se limpia en la siguiente instalación.
149
206
 
@@ -187,9 +244,11 @@ instalación la haya lanzado SYSTEM.
187
244
  - **Renderer**: a los 10 s de arrancar y después cada hora mira `version_renderer.txt` en el
188
245
  bucket de S3 y baja el zip a `%APPDATA%\glassnote\renderer`. No toca el binario.
189
246
 
190
- Salidas a internet que necesita el equipo: `registry.npmjs.org` (instalar y actualizar),
191
- `glassnote.s3.us-east-1.amazonaws.com` (renderer), `nodejs.org` (solo si no hay Node) y
192
- la API.
247
+ Salidas a internet que necesita el equipo con el instalador genérico: `registry.npmjs.org`
248
+ (instalar y actualizar), `glassnote.s3.us-east-1.amazonaws.com` (renderer), `nodejs.org`
249
+ (solo si no hay Node) y la API. Con el instalador SCCM offline, la instalación inicial
250
+ usa `glassnote.intermark.ec` para descargar el ZIP y la API para registrar el dispositivo;
251
+ `registry.npmjs.org` queda para actualizaciones posteriores del cliente.
193
252
 
194
253
  Sin probar, pero a revisar con el área de sistemas del cliente:
195
254
 
@@ -240,7 +299,7 @@ días, lo que acota lo que se puede hacer con una que se filtre.
240
299
  Un equipo ya instalado se empareja sin reinstalar:
241
300
 
242
301
  ```sh
243
- glassnote pair --org-id 55 --api-key <clave> --device-name "Caja 01"
302
+ glassnote pair --org-id <id> --api-key <clave> --device-name "Caja 01"
244
303
  ```
245
304
 
246
305
  Si el alta falla, el comando **sale con código distinto de cero** y dice por qué (la
package/autoStart.js CHANGED
@@ -144,7 +144,7 @@ class AutoStart {
144
144
 
145
145
  fs.writeFileSync(tempScriptPath, script);
146
146
 
147
- execSync(`cscript //nologo "${tempScriptPath}"`);
147
+ execSync(`cscript //nologo "${tempScriptPath}"`, { windowsHide: true });
148
148
 
149
149
  fs.unlinkSync(tempScriptPath);
150
150
 
package/bin/glassnote.js CHANGED
@@ -72,7 +72,7 @@ function npmInvocation(args) {
72
72
 
73
73
  function runNpm(args, opts) {
74
74
  const { file, args: argv, shell } = npmInvocation(args);
75
- return spawnSync(file, argv, Object.assign({ stdio: 'inherit', shell, env: npmEnv() }, opts || {}));
75
+ return spawnSync(file, argv, Object.assign({ stdio: 'inherit', shell, env: npmEnv(), windowsHide: true }, opts || {}));
76
76
  }
77
77
 
78
78
  // userData tira de `uuid`, y este CLI corre también donde el árbol de dependencias no
@@ -113,6 +113,7 @@ function clienteEnMarcha() {
113
113
  env: Object.assign({}, process.env, { ELECTRON_RUN_AS_NODE: '1' }),
114
114
  stdio: 'ignore',
115
115
  timeout: 4000,
116
+ windowsHide: true,
116
117
  });
117
118
  return res.status === 0;
118
119
  }
@@ -193,7 +194,7 @@ function cerrarClienteWindows() {
193
194
  const res = spawnSync(
194
195
  'powershell.exe',
195
196
  ['-NoProfile', '-NonInteractive', '-Command', guionCerrarCliente(rutaDelClienteInstalado(), process.pid)],
196
- { encoding: 'utf8', timeout: 20000 }
197
+ { encoding: 'utf8', timeout: 20000, windowsHide: true }
197
198
  );
198
199
 
199
200
  // Si PowerShell no está o falló, no se sigue a ciegas: se mira si el cliente
@@ -272,12 +273,12 @@ function launch({ detach = false, extra = [] } = {}) {
272
273
  }
273
274
 
274
275
  if (detach) {
275
- const child = spawn(binary, args, { detached: true, stdio: 'ignore', env });
276
+ const child = spawn(binary, args, { detached: true, stdio: 'ignore', env, windowsHide: true });
276
277
  child.unref();
277
278
  return 0;
278
279
  }
279
280
 
280
- const child = spawnSync(binary, args, { stdio: 'inherit', env });
281
+ const child = spawnSync(binary, args, { stdio: 'inherit', env, windowsHide: true });
281
282
  if (child.error) fail(`no se pudo arrancar el cliente: ${child.error.message}`);
282
283
  if (child.status !== 0 && process.platform === 'linux') {
283
284
  console.error(
@@ -328,6 +329,7 @@ function addToPathWindows(dir = mode.BIN_DIR) {
328
329
  ].join('; ');
329
330
  const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], {
330
331
  encoding: 'utf8',
332
+ windowsHide: true,
331
333
  });
332
334
  const added = res.status === 0 && String(res.stdout).includes('added');
333
335
  return { touched: added ? ['PATH de usuario (sin administrador)'] : [], line: dir };
@@ -389,6 +391,7 @@ function removeFromPathWindows() {
389
391
  ].join('; ');
390
392
  const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], {
391
393
  encoding: 'utf8',
394
+ windowsHide: true,
392
395
  });
393
396
  return res.status === 0 && String(res.stdout).includes('removed');
394
397
  }
@@ -633,10 +636,14 @@ async function cmdInstall(args) {
633
636
  // Al arrancar, la app se registra sola en el autoarranque del usuario.
634
637
  step('arrancando el cliente en segundo plano (queda en la bandeja del sistema)');
635
638
  const binary = path.join(mode.INSTALLED_PACKAGE_DIR, 'bin', 'glassnote.js');
639
+ // windowsHide: en Windows un proceso desprendido abre su propia consola, y la gente la
640
+ // cierra. Lo mismo vale para cada spawn de este archivo: el ayudante de actualización
641
+ // corre sin consola, y ahí cualquier hijo de consola sin windowsHide abriría una.
636
642
  const child = spawn(process.execPath, [binary, 'start', '--detach'], {
637
643
  detached: true,
638
644
  stdio: 'ignore',
639
645
  env: process.env,
646
+ windowsHide: true,
640
647
  });
641
648
  child.unref();
642
649
  log('Listo. Ícono en la bandeja; se abrirá solo en los próximos arranques.');
package/main.js CHANGED
@@ -6,6 +6,7 @@ const {
6
6
  Menu,
7
7
  nativeImage,
8
8
  screen,
9
+ session,
9
10
  } = require('electron');
10
11
  const packageJson = require('./package.json');
11
12
  const LocalServer = require('./localserver');
@@ -231,6 +232,8 @@ setInterval(cleanupOldLogs, LIMPIEZA_LOGS_MS).unref();
231
232
  createTailUtility(logsDir);
232
233
 
233
234
  app.whenReady().then(() => {
235
+ allowYoutubeEmbeds();
236
+
234
237
  // macOS: esto es una app de bandeja, se maneja desde la barra de menu. Instalada por
235
238
  // npm no hay .app propio, asi que el Dock mostraba el icono por defecto de Electron y
236
239
  // una entrada que no sirve para nada. Se saca del Dock, pero antes se le pone el icono
@@ -865,23 +868,34 @@ function iniciarSondeoCursor() {
865
868
  }
866
869
  if (!dentro) return;
867
870
 
868
- // De aca para abajo es solo de Linux. En Windows el `forward: true` de
869
- // setIgnoreMouseEvents ya entrega los movimientos reales con la ventana ignorando
870
- // el mouse —medido: el boton recibe mouseover y se puede clickear—, asi que
871
- // inyectarlos seria duplicarlos.
872
- if (!enLinux) return;
873
-
874
- // Con el overlay ya clickeable los eventos reales llegan solos: inyectar seria
875
- // duplicarlos.
876
- if (!ignorandoMouse) return;
871
+ // En Linux se inyecta mientras el overlay ignora el mouse: ahi no llega nada, y con
872
+ // la ventana clickeable los eventos reales llegan solos.
873
+ //
874
+ // En Windows es AL REVES. Mientras ignora, el `forward: true` de setIgnoreMouseEvents
875
+ // ya entrega los movimientos reales —medido: el boton recibe mouseover y se puede
876
+ // clickear—. Pero con la ventana clickeable Windows NO le manda todos: el clic que
877
+ // activa el overlay (desde 2.4.12 se muestra sin foco, asi que el primer clic sobre
878
+ // un formulario lo activa) le devuelve el WS_EX_LAYERED a los ~30 ms, y una ventana
879
+ // layered deja pasar el mouse por sus pixeles transparentes a la ventana de abajo.
880
+ // El cursor sale del formulario, el renderer no recibe un solo movimiento, se queda
881
+ // con la ultima posicion —encima del formulario— y nunca devuelve el passthrough: el
882
+ // overlay queda clickeable y se come los clics sobre todo lo que dibuja. Medido en
883
+ // la VM de Windows 11 con el estilo de la ventana (0x80008 en vez de 0x80028) y con
884
+ // el estado del renderer; en 2.4.11, con la ventana ya activa, no pasaba.
885
+ // Se le cuenta la posicion desde aca y el renderer se pone al dia solo.
886
+ // En macOS no se midio nada de esto: queda como estaba.
887
+ if (process.platform === 'darwin') return;
888
+ if (enLinux ? !ignorandoMouse : ignorandoMouse) return;
877
889
 
878
890
  // Repetir el mismo punto en cada tick es ruido, pero callarse del todo mientras el
879
891
  // cursor esta quieto rompe el otro caso: el menu se abre DEBAJO de un cursor que no
880
892
  // se movio, no hay mousemove que lo cuente, y ese boton nace muerto. Se reemite cada
881
893
  // tanto para que la UI que aparece sola se entere igual.
894
+ // En Windows solo cuando el cursor se mueve: el menu que aparece bajo un cursor
895
+ // quieto ya lo cubre el forward, y repetir el punto no le cuenta nada nuevo.
882
896
  const ahora = Date.now();
883
897
  const mismoPunto = x === ultimoPuntoEnviado.x && y === ultimoPuntoEnviado.y;
884
- if (mismoPunto && ahora - ultimoEnvioMs < REEMISION_QUIETO_MS) return;
898
+ if (mismoPunto && (!enLinux || ahora - ultimoEnvioMs < REEMISION_QUIETO_MS)) return;
885
899
  ultimoPuntoEnviado = { x, y };
886
900
  ultimoEnvioMs = ahora;
887
901
  mainWindow.webContents.sendInputEvent({ type: 'mouseMove', x, y });
@@ -1057,6 +1071,41 @@ process.on('unhandledRejection', (reason, promise) => {
1057
1071
 
1058
1072
 
1059
1073
 
1074
+ /**
1075
+ * YouTube rechaza el reproductor embebido que llega sin Referer: muestra "Video player
1076
+ * configuration error" (el error 153) en vez del video. El renderer se carga desde file://
1077
+ * y de ahi Chromium no manda Referer a ningun sitio https, asi que un glass con un video
1078
+ * nunca lo reproduciria. Medido sobre Electron 29 con el mismo video: sin esto se queda en
1079
+ * 0 s con el error; con esto reproduce, con sonido.
1080
+ *
1081
+ * Solo toca el pedido del reproductor (/embed/) y solo si no trae Referer: la
1082
+ * previsualizacion web, que va por https, manda el suyo y no pasa por aca.
1083
+ *
1084
+ * OJO: Electron admite UN solo onBeforeSendHeaders por sesion. Registrar otro en cualquier
1085
+ * lado reemplaza a este sin avisar, y los videos vuelven a dar el 153.
1086
+ */
1087
+ function allowYoutubeEmbeds() {
1088
+ try {
1089
+ session.defaultSession.webRequest.onBeforeSendHeaders(
1090
+ {
1091
+ urls: [
1092
+ 'https://www.youtube.com/embed/*',
1093
+ 'https://www.youtube-nocookie.com/embed/*',
1094
+ ],
1095
+ },
1096
+ (details, callback) => {
1097
+ const headers = details.requestHeaders;
1098
+ if (!headers.Referer && !headers.referer) {
1099
+ headers.Referer = 'https://glassnote.intermark.ec/';
1100
+ }
1101
+ callback({ requestHeaders: headers });
1102
+ }
1103
+ );
1104
+ } catch (error) {
1105
+ console.error('[youtube] no se pudo preparar el Referer de los videos:', error.message);
1106
+ }
1107
+ }
1108
+
1060
1109
  /**
1061
1110
  * Carga el HTML del renderer. Cual de las dos fuentes gana lo decide resolverRenderer()
1062
1111
  * en rendererPaths.js, que es tambien de donde sale la version que muestra el menu.
package/npmUpdate.js CHANGED
@@ -238,12 +238,14 @@ class NpmUpdate {
238
238
  }
239
239
 
240
240
  try {
241
+ // windowsHide: sin él Windows le abre una consola al ayudante, que dura todo el
242
+ // npm install, y si el usuario la cierra la actualización muere a medias.
241
243
  // cwd fuera del paquete: en Windows no se puede borrar un directorio que es
242
244
  // el cwd de un proceso vivo, y ese proceso sería justamente el actualizador.
243
245
  const child = spawn(
244
246
  nodeBinary || process.execPath,
245
247
  [cli, 'update', version, '--relaunch', '--wait', '4000'],
246
- { detached: true, stdio: ['ignore', salida, salida], env, cwd: os.tmpdir() }
248
+ { detached: true, stdio: ['ignore', salida, salida], env, cwd: os.tmpdir(), windowsHide: true }
247
249
  );
248
250
  child.unref();
249
251
  console.warn(`[update] ayudante lanzado, pid ${child.pid}; la app se cierra en 500 ms`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassnote/client",
3
- "version": "2.4.16",
3
+ "version": "2.4.18",
4
4
  "description": "GlassNote — cliente de escritorio de notas superpuestas. Se instala con npx, sin instaladores nativos ni firmas.",
5
5
  "main": "main.js",
6
6
  "publishConfig": {
@@ -75,7 +75,7 @@
75
75
  "url": "git+ssh://git@intermarkec/intermarkec/glassnote-electron.git"
76
76
  },
77
77
  "dependencies": {
78
- "@glassnote/renderer": "2.2.1",
78
+ "@glassnote/renderer": "2.3.2",
79
79
  "cors": "^2.8.5",
80
80
  "electron": "^29.4.6",
81
81
  "express": "^4.18.2",