xonecode 0.35.0 → 0.36.3
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/THIRD_PARTY_NOTICES.md +30 -0
- package/apps/web/dist/assets/{EditorDeFichero-CGZ6uZPa.js → EditorDeFichero-B9EyiuM6.js} +1 -1
- package/apps/web/dist/assets/index-C6MsC7A5.js +506 -0
- package/apps/web/dist/assets/index-DRNhZyYF.css +1 -0
- package/apps/web/dist/assets/{php-DTPkbJvq.js → php-DCP5mtTp.js} +1 -1
- package/apps/web/dist/assets/{ruby-D1wBOK7G.js → ruby-B41ayseU.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/dist/agent/config/settingsEnDisco.js +30 -0
- package/dist/agent/config/settingsEnDisco.js.map +1 -1
- package/dist/agent/dispositivos/instalacionEnMaquina.js +13 -0
- package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -1
- package/dist/agent/motores/trueforge/sesionTrueforge.js +39 -1
- package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
- package/dist/agent/remoto/ngrokEnMaquina.js +113 -0
- package/dist/agent/remoto/ngrokEnMaquina.js.map +1 -0
- package/dist/agent/remoto/puenteWebSocket.js +175 -0
- package/dist/agent/remoto/puenteWebSocket.js.map +1 -0
- package/dist/agent/remoto/tunelNgrok.js +245 -0
- package/dist/agent/remoto/tunelNgrok.js.map +1 -0
- package/dist/core/dispositivos.js +53 -0
- package/dist/core/dispositivos.js.map +1 -1
- package/dist/core/idiomaDeLaRespuesta.js +38 -0
- package/dist/core/idiomaDeLaRespuesta.js.map +1 -0
- package/dist/core/panorama.js +93 -0
- package/dist/core/panorama.js.map +1 -0
- package/dist/core/ports.js +100 -0
- package/dist/core/ports.js.map +1 -1
- package/dist/core/remoto.js +117 -0
- package/dist/core/remoto.js.map +1 -0
- package/dist/core/settings.js +83 -0
- package/dist/core/settings.js.map +1 -1
- package/dist/core/tunel.js +92 -0
- package/dist/core/tunel.js.map +1 -0
- package/dist/web/servidor/arranque.js +597 -72
- package/dist/web/servidor/arranque.js.map +1 -1
- package/dist/web/servidor/consolaWeb.js +52 -2
- package/dist/web/servidor/consolaWeb.js.map +1 -1
- package/dist/web/servidor/puenteRemoto.js +272 -0
- package/dist/web/servidor/puenteRemoto.js.map +1 -0
- package/dist/web/servidor/transporte.js.map +1 -1
- package/dist/web/servidor/vestibulo.js +39 -1
- package/dist/web/servidor/vestibulo.js.map +1 -1
- package/node_modules/@xone/xonecode-remoto/dist/base64url.d.ts +2 -0
- package/node_modules/@xone/xonecode-remoto/dist/base64url.js +19 -0
- package/node_modules/@xone/xonecode-remoto/dist/cifrado.d.ts +25 -0
- package/node_modules/@xone/xonecode-remoto/dist/cifrado.js +75 -0
- package/node_modules/@xone/xonecode-remoto/dist/contenido.d.ts +121 -0
- package/node_modules/@xone/xonecode-remoto/dist/contenido.js +191 -0
- package/node_modules/@xone/xonecode-remoto/dist/index.d.ts +6 -0
- package/node_modules/@xone/xonecode-remoto/dist/index.js +6 -0
- package/node_modules/@xone/xonecode-remoto/dist/servidor/salas.d.ts +46 -0
- package/node_modules/@xone/xonecode-remoto/dist/servidor/salas.js +128 -0
- package/node_modules/@xone/xonecode-remoto/dist/servidor/servidor.d.ts +31 -0
- package/node_modules/@xone/xonecode-remoto/dist/servidor/servidor.js +198 -0
- package/node_modules/@xone/xonecode-remoto/dist/sobre.d.ts +53 -0
- package/node_modules/@xone/xonecode-remoto/dist/sobre.js +69 -0
- package/node_modules/@xone/xonecode-remoto/dist/url.d.ts +12 -0
- package/node_modules/@xone/xonecode-remoto/dist/url.js +35 -0
- package/node_modules/@xone/xonecode-remoto/dist/version.d.ts +2 -0
- package/node_modules/@xone/xonecode-remoto/dist/version.js +2 -0
- package/node_modules/@xone/xonecode-remoto/package.json +41 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/index-Bfa2gK3J.css +1 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/index-qBo9eRDN.js +77 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/node_modules/@xone/xonecode-remoto/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/node_modules/@xone/xonecode-remoto/web/index.html +14 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +22 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/node_modules/ws/lib/receiver.js +743 -0
- package/node_modules/ws/lib/sender.js +607 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +562 -0
- package/node_modules/ws/lib/websocket.js +1407 -0
- package/node_modules/ws/package.json +74 -0
- package/node_modules/ws/wrapper.mjs +21 -0
- package/package.json +6 -1
- package/apps/web/dist/assets/index-BBQH-l_3.css +0 -1
- package/apps/web/dist/assets/index-DqYXrrxo.js +0 -499
|
@@ -52,7 +52,9 @@ import { detectarDispositivos, frameworkEnDispositivo } from "../../agent/dispos
|
|
|
52
52
|
*/
|
|
53
53
|
import { lanzarEnDispositivo, } from "../../agent/dispositivos/lanzamientoEnMaquina.js";
|
|
54
54
|
import { motivoDeBloqueo, puedeLanzarse, } from "../../core/puedeLanzarse.js";
|
|
55
|
-
import { PLATAFORMAS_DE_DISPOSITIVO } from "../../core/settings.js";
|
|
55
|
+
import { PLATAFORMAS_DE_DISPOSITIVO, remotoEnVigor } from "../../core/settings.js";
|
|
56
|
+
import { recetaDeNgrok } from "../../core/dispositivos.js";
|
|
57
|
+
import { motivoDeDominioInaceptable } from "../../core/tunel.js";
|
|
56
58
|
import { instalarHerramientaDeDispositivos, verificarDispositivo } from "../../agent/dispositivos/dispositivosEnMaquina.js";
|
|
57
59
|
import { arrancarEmulador, pararEmulador } from "../../agent/dispositivos/arranqueDeEmulador.js";
|
|
58
60
|
import { borrarAvd, clonarAvd, correrPasoDeReceta, crearAvd } from "../../agent/dispositivos/instalacionEnMaquina.js";
|
|
@@ -72,7 +74,7 @@ import { borrarCredencial, guardarCredencial } from "../../agent/config/authEnDi
|
|
|
72
74
|
import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedoresPersonalizados, } from "../../agent/config/configEnDisco.js";
|
|
73
75
|
import { crearCheckpointerDeProyecto, mantenimientoDelCheckpointer, } from "../../agent/sesiones/checkpointer.js";
|
|
74
76
|
import { resumenDePoda } from "../../core/podaDeCheckpoint.js";
|
|
75
|
-
import { cargarSettings, guardarConcurrenciaDeTareas, guardarDepurar, guardarModoPorDefecto, guardarWorkspace as guardarWorkspaceEnDisco, guardarDispositivos, guardarAjusteDeAvd, quitarAjustesDeAvd, guardarPuertosAsignados, guardarEntorno as guardarEntornoEnDisco, olvidarEntornoDeSettings, copiasDeEntorno, borrarCopiasDeEntorno, } from "../../agent/config/settingsEnDisco.js";
|
|
77
|
+
import { cargarSettings, guardarConcurrenciaDeTareas, guardarDepurar, guardarModoPorDefecto, guardarAjustesDeRemoto as guardarAjustesDeRemotoEnDisco, guardarWorkspace as guardarWorkspaceEnDisco, guardarDispositivos, guardarAjusteDeAvd, quitarAjustesDeAvd, guardarPuertosAsignados, guardarEntorno as guardarEntornoEnDisco, olvidarEntornoDeSettings, copiasDeEntorno, borrarCopiasDeEntorno, } from "../../agent/config/settingsEnDisco.js";
|
|
76
78
|
import { dentroDelWorkspace, depuracionActiva, expandirConCasa, mismaRuta, motivoParaNoOlvidarEntorno, motivoDeNombreDeEntornoInaceptable, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
|
|
77
79
|
import { esModoDeEscritura, MODO_POR_OMISION, } from "../../core/modoDeEscritura.js";
|
|
78
80
|
import { megasDeLasMudanzas, mudarWorkspaceLegado } from "../../agent/config/mudanzaEnDisco.js";
|
|
@@ -115,6 +117,8 @@ import { baseDeWorkspaceLegada, baseDeWorkspacePorOmision, conexionDeVestibulo,
|
|
|
115
117
|
// documentado cinco veces: una composición de producción dentro de algo que los tests
|
|
116
118
|
// doblan.
|
|
117
119
|
import { filaDeTarea } from "./transporte.js";
|
|
120
|
+
import { crearSesionRemota, motivoFijoDelTunel } from "./puenteRemoto.js";
|
|
121
|
+
import { componerPanorama, INTERVALO_DE_PANORAMA_MS } from "../../core/panorama.js";
|
|
118
122
|
import { esEsfuerzo, nivelesDeEsfuerzo } from "../../core/esfuerzo.js";
|
|
119
123
|
import { CATALOGO_DE_CONECTORES, conectoresParaElAgente, definicionDelCable, RUTA_CALLBACK_MCP } from "../../core/conectores.js";
|
|
120
124
|
import { servicioDeConectoresCableado } from "../../agent/conectores/servicioDeConectores.js";
|
|
@@ -351,6 +355,44 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
351
355
|
* Emitir es escribirle a todos; el transporte hace lo mismo por su lado.
|
|
352
356
|
*/
|
|
353
357
|
const clientes = new Set();
|
|
358
|
+
/**
|
|
359
|
+
* La sesión remota, si esta ejecución la monta. Se ASIGNA más abajo, en cuanto existen
|
|
360
|
+
* `engancharCliente`/`soltarCliente`/`cancelarTurno`/`recibirEnLaConsola`, que son su puerta y
|
|
361
|
+
* su despacho; se declara aquí porque la ráfaga (`adjuntar`) también la lee.
|
|
362
|
+
*/
|
|
363
|
+
let sesionRemota;
|
|
364
|
+
const remotoHabilitado = () => opciones.remoto?.ajustes().habilitado === true;
|
|
365
|
+
/** La última foto de ngrok (`OpcionesDeMontaje.remoto.medirNgrok`); `undefined` = aún no medida. */
|
|
366
|
+
let fotoDeNgrok;
|
|
367
|
+
/** Por qué falló la última acción de ngrok: SOLO un texto fijo nuestro. Viaja en la foto. */
|
|
368
|
+
let motivoDeNgrok;
|
|
369
|
+
const mensajeDeRemoto = (e) => {
|
|
370
|
+
const ajustes = opciones.remoto?.ajustes();
|
|
371
|
+
return {
|
|
372
|
+
clase: "remoto",
|
|
373
|
+
estado: e.estado,
|
|
374
|
+
// El interruptor, para Ajustes: la sección se pinta siempre y el botón del chat solo encendido.
|
|
375
|
+
habilitado: remotoHabilitado(),
|
|
376
|
+
...(ajustes?.porEntorno === true ? { porEntorno: true } : {}),
|
|
377
|
+
...("url" in e ? { url: e.url } : {}),
|
|
378
|
+
...("moviles" in e ? { moviles: e.moviles } : {}),
|
|
379
|
+
...("motivo" in e ? { motivo: e.motivo } : {}),
|
|
380
|
+
// La vía EN VIGOR, siempre (`remotoEnVigor` ya resolvió la omisión): el cliente toma una
|
|
381
|
+
// ausente por ngrok, así que callarla con «servidor» lo pintaría como ngrok.
|
|
382
|
+
...(ajustes?.via !== undefined ? { via: ajustes.via } : {}),
|
|
383
|
+
...(fotoDeNgrok === undefined
|
|
384
|
+
? {}
|
|
385
|
+
: {
|
|
386
|
+
ngrok: {
|
|
387
|
+
instalado: fotoDeNgrok.instalado,
|
|
388
|
+
cuenta: fotoDeNgrok.cuenta,
|
|
389
|
+
...(ajustes?.dominio === undefined ? {} : { dominio: ajustes.dominio }),
|
|
390
|
+
...(fotoDeNgrok.instalado ? {} : { receta: recetaDeNgrok(process.platform, fotoDeNgrok) }),
|
|
391
|
+
...(motivoDeNgrok === undefined ? {} : { motivo: motivoDeNgrok }),
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
394
|
+
};
|
|
395
|
+
};
|
|
354
396
|
/**
|
|
355
397
|
* Los clientes por su IDENTIFICADOR, y qué tareas está mirando cada uno.
|
|
356
398
|
*
|
|
@@ -439,6 +481,101 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
439
481
|
for (const cliente of clientes)
|
|
440
482
|
cliente(mensaje);
|
|
441
483
|
};
|
|
484
|
+
/**
|
|
485
|
+
* El PANEL de la sesión remota (`core/panorama.ts`): las consolas de persona abiertas y la cola
|
|
486
|
+
* de tareas, compuesto AQUÍ y mandado SOLO a los móviles (`SesionRemota.difundir`), nunca a las
|
|
487
|
+
* pestañas. Cada consola cruza con un id OPACO por encendido de la sesión remota —raíz → id, que
|
|
488
|
+
* se olvida al apagar—: la raíz es una ruta de la máquina (`sinRutas`) y el proyecto viaja como
|
|
489
|
+
* su `basename`. **Límite declarado**: el mismo nombre de proyecto en dos entornos se confunde.
|
|
490
|
+
*/
|
|
491
|
+
const idsDeConsola = new Map();
|
|
492
|
+
const idDeConsola = (raiz) => {
|
|
493
|
+
let id = idsDeConsola.get(raiz);
|
|
494
|
+
if (id === undefined) {
|
|
495
|
+
id = randomUUID();
|
|
496
|
+
idsDeConsola.set(raiz, id);
|
|
497
|
+
}
|
|
498
|
+
return id;
|
|
499
|
+
};
|
|
500
|
+
/** La consola de persona ABIERTA con ese id, o nada: un id de otro encendido, o de una consola
|
|
501
|
+
* que ya se cerró, no resuelve a ninguna. */
|
|
502
|
+
const consolaDelPanel = (id) => {
|
|
503
|
+
for (const [raiz, suyo] of idsDeConsola) {
|
|
504
|
+
if (suyo === id)
|
|
505
|
+
return vestibulo.proyectosAbiertos().find((c) => c.raiz === raiz);
|
|
506
|
+
}
|
|
507
|
+
return undefined;
|
|
508
|
+
};
|
|
509
|
+
const aprobacionEnVueloDe = (c) => {
|
|
510
|
+
for (const m of c.mensajesDeAprobacion())
|
|
511
|
+
if (m.clase === "aprobacion")
|
|
512
|
+
return m;
|
|
513
|
+
return undefined;
|
|
514
|
+
};
|
|
515
|
+
const panoramaAhora = () => {
|
|
516
|
+
const foco = vestibulo.proyectoAbierto();
|
|
517
|
+
const consolas = vestibulo.proyectosAbiertos().map((c) => {
|
|
518
|
+
const fila = c.sesion === undefined ? undefined : vestibulo.sesionesDe(c.raiz).find((s) => s.id === c.sesion);
|
|
519
|
+
const aprobacion = aprobacionEnVueloDe(c);
|
|
520
|
+
return {
|
|
521
|
+
id: idDeConsola(c.raiz),
|
|
522
|
+
proyecto: basename(c.raiz),
|
|
523
|
+
...(fila === undefined || fila.titulo === "" ? {} : { sesion: fila.titulo }),
|
|
524
|
+
foco: c === foco,
|
|
525
|
+
turnoEnVuelo: c.turnoEnVuelo,
|
|
526
|
+
...(aprobacion === undefined
|
|
527
|
+
? {}
|
|
528
|
+
: { aprobacion: { pendientes: aprobacion.pendientes, ficheros: aprobacion.ficheros, diffs: aprobacion.diffs } }),
|
|
529
|
+
preguntaPendiente: c.preguntaPendiente,
|
|
530
|
+
...(fila?.ultimoTurno === undefined ? {} : { ultimaActividad: fila.ultimoTurno }),
|
|
531
|
+
};
|
|
532
|
+
});
|
|
533
|
+
const tareas = (opciones.colaDeTareas?.listar() ?? []).map((t) => ({
|
|
534
|
+
id: t.id,
|
|
535
|
+
titulo: t.titulo,
|
|
536
|
+
// El NOMBRE, como en `filaDeTarea`: la raíz de la tarea no viaja.
|
|
537
|
+
proyecto: t.proyecto.nombre,
|
|
538
|
+
estado: t.estado,
|
|
539
|
+
creada: t.creada,
|
|
540
|
+
...(t.tras === undefined ? {} : { tras: t.tras }),
|
|
541
|
+
...(t.motivo === undefined ? {} : { motivo: t.motivo }),
|
|
542
|
+
...(t.acabada === undefined ? {} : { acabada: t.acabada }),
|
|
543
|
+
}));
|
|
544
|
+
return componerPanorama({ consolas, tareas });
|
|
545
|
+
};
|
|
546
|
+
/**
|
|
547
|
+
* Un panorama nuevo a todos los móviles, como mucho uno por `INTERVALO_DE_PANORAMA_MS` y
|
|
548
|
+
* siempre DESPUÉS del último cambio. Los gatillos (`alCambiarEsperas`, `emitirTareas`) saltan a
|
|
549
|
+
* menudo y desde dentro de una resolución (una aprobación que se contesta): aquí solo se
|
|
550
|
+
* PROGRAMA, y se compone al vencer el reloj, con el estado de ese instante.
|
|
551
|
+
*/
|
|
552
|
+
let relojDePanorama;
|
|
553
|
+
let ultimoPanorama = Number.NEGATIVE_INFINITY;
|
|
554
|
+
const programarPanorama = () => {
|
|
555
|
+
if (relojDePanorama !== undefined || sesionRemota === undefined || !sesionRemota.hayMoviles())
|
|
556
|
+
return;
|
|
557
|
+
const espera = Math.max(0, ultimoPanorama + INTERVALO_DE_PANORAMA_MS - Date.now());
|
|
558
|
+
relojDePanorama = setTimeout(() => {
|
|
559
|
+
relojDePanorama = undefined;
|
|
560
|
+
if (sesionRemota === undefined || !sesionRemota.hayMoviles())
|
|
561
|
+
return;
|
|
562
|
+
ultimoPanorama = Date.now();
|
|
563
|
+
try {
|
|
564
|
+
sesionRemota.difundir(panoramaAhora());
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
contar(error);
|
|
568
|
+
}
|
|
569
|
+
}, espera);
|
|
570
|
+
relojDePanorama.unref?.();
|
|
571
|
+
};
|
|
572
|
+
const olvidarPanorama = () => {
|
|
573
|
+
if (relojDePanorama !== undefined)
|
|
574
|
+
clearTimeout(relojDePanorama);
|
|
575
|
+
relojDePanorama = undefined;
|
|
576
|
+
idsDeConsola.clear();
|
|
577
|
+
};
|
|
578
|
+
vestibulo.alCambiarEsperas(programarPanorama);
|
|
442
579
|
/**
|
|
443
580
|
* Lo que valga esa promesa, o nada si tarda demasiado. **No la cancela**: sigue viva y su
|
|
444
581
|
* valor se usará en el siguiente anuncio — lo que se acota es la ESPERA, no el trabajo.
|
|
@@ -507,7 +644,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
507
644
|
* hace que se elija TAMBIÉN el modelo. El wizard sigue sabiendo pintarlo por si otra
|
|
508
645
|
* piel se lo manda; esta no.
|
|
509
646
|
*/
|
|
510
|
-
const
|
|
647
|
+
const componerAlta = async () => {
|
|
511
648
|
const abierto = vestibulo.proyectoAbierto();
|
|
512
649
|
const proyectoAbierto = abierto !== undefined;
|
|
513
650
|
// Se lee del disco EN CADA anuncio y no se cachea al abrir: `configurarModoInicial`
|
|
@@ -570,7 +707,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
570
707
|
conIcono.add(p.id);
|
|
571
708
|
}));
|
|
572
709
|
}
|
|
573
|
-
|
|
710
|
+
return {
|
|
574
711
|
clase: "alta",
|
|
575
712
|
pasos,
|
|
576
713
|
proveedores: PROVEEDORES.map((p) => ({ id: p, nombre: nombreDeProveedor(p) })),
|
|
@@ -680,7 +817,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
680
817
|
// en cada anuncio en vez de pasarse por parámetro: los anuncios de los flancos del
|
|
681
818
|
// turno no preparan nada y tienen que decir lo que hay, no lo que había.
|
|
682
819
|
...(preparando === undefined ? {} : { preparando }),
|
|
683
|
-
}
|
|
820
|
+
};
|
|
821
|
+
};
|
|
822
|
+
/** El alta a TODOS los clientes vivos. Compuesto aparte (`componerAlta`) para poder dárselo a
|
|
823
|
+
* UNO solo: el móvil que entra por la sesión remota (ver `engancharMovil`). */
|
|
824
|
+
const anunciarAlta = async () => {
|
|
825
|
+
emitir(await componerAlta());
|
|
684
826
|
};
|
|
685
827
|
/**
|
|
686
828
|
* Los catálogos ya consultados en ESTE proceso, por proveedor. Se guardan porque cada
|
|
@@ -1169,6 +1311,16 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
1169
1311
|
cliente(depuracion);
|
|
1170
1312
|
if (modoPorDefecto !== undefined)
|
|
1171
1313
|
cliente(modoPorDefecto);
|
|
1314
|
+
// La sesión remota, SIEMPRE que esta ejecución la monta: con `habilitado`, que es lo que
|
|
1315
|
+
// pinta el interruptor de Ajustes (y, apagado, no hay botón en el chat). Un puente abierto
|
|
1316
|
+
// con el interruptor quitado a mano en `settings.json` sale igual, para poder apagarlo.
|
|
1317
|
+
// A un móvil no le llega: `remoto` no pasa `filtrarSalida` (lleva el secreto en la `url`).
|
|
1318
|
+
if (sesionRemota !== undefined)
|
|
1319
|
+
cliente(mensajeDeRemoto(sesionRemota.estado()));
|
|
1320
|
+
// La foto de ngrok, la PRIMERA vez que alguien mira: lanza `ngrok version`, que no se hace
|
|
1321
|
+
// con el interruptor apagado ni en sondeo.
|
|
1322
|
+
if (sesionRemota !== undefined && fotoDeNgrok === undefined && midiendoNgrok === undefined && remotoHabilitado())
|
|
1323
|
+
void medirNgrokYAnunciar().catch(contar);
|
|
1172
1324
|
if (conectores !== undefined)
|
|
1173
1325
|
cliente(conectores);
|
|
1174
1326
|
if (consumoDeLaSesion !== undefined) {
|
|
@@ -1436,6 +1588,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
1436
1588
|
const m = mensajeDeTareas();
|
|
1437
1589
|
if (m !== undefined)
|
|
1438
1590
|
emitir(m);
|
|
1591
|
+
programarPanorama();
|
|
1439
1592
|
};
|
|
1440
1593
|
/**
|
|
1441
1594
|
* Un id de proyecto a su tripleta completa, o nada si no se puede resolver.
|
|
@@ -1799,6 +1952,24 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
1799
1952
|
informar(`no se pudo añadir el feedback: ${resultado.motivo ?? "motivo desconocido"}`);
|
|
1800
1953
|
}
|
|
1801
1954
|
};
|
|
1955
|
+
/**
|
|
1956
|
+
* El feedback y «Continuar» de una tarea aparcada: los MISMOS para los botones del escritorio
|
|
1957
|
+
* (`POST /accion`) y para el panel del móvil, que no es un segundo camino con su propia regla.
|
|
1958
|
+
*/
|
|
1959
|
+
const tareaEsperandoFeedback = (id) => opciones.colaDeTareas?.listar().find((t) => t.id === id)?.estado === "requiere-atencion";
|
|
1960
|
+
const darFeedbackATarea = (id, texto) => {
|
|
1961
|
+
atenderFeedbackDeTarea(id, texto);
|
|
1962
|
+
opciones.revisarTareas?.();
|
|
1963
|
+
emitirTareas();
|
|
1964
|
+
};
|
|
1965
|
+
/** Lo mismo que un feedback, con el texto de «sigue» del núcleo: entra en SU hilo, no repite el
|
|
1966
|
+
* encargo. Devuelve por qué no, o nada si se hizo. */
|
|
1967
|
+
const continuarTarea = (id) => {
|
|
1968
|
+
if (!tareaEsperandoFeedback(id))
|
|
1969
|
+
return "esa tarea ya no está esperando feedback";
|
|
1970
|
+
darFeedbackATarea(id, TEXTO_DE_CONTINUAR);
|
|
1971
|
+
return undefined;
|
|
1972
|
+
};
|
|
1802
1973
|
/** Los ajustes de destino, o `{}` —que es «se miran todos»— si esta ejecución no los lee. */
|
|
1803
1974
|
const ajustesDeDispositivos = () => opciones.ajustesDeDispositivos?.() ?? {};
|
|
1804
1975
|
/**
|
|
@@ -4206,7 +4377,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4206
4377
|
ultimoProgreso = 0;
|
|
4207
4378
|
emitirProgreso(resultado.estado, resultado.motivo);
|
|
4208
4379
|
trabajo = undefined;
|
|
4209
|
-
// Y la foto nueva es la que dice si el paso quedó hecho, no el código de salida.
|
|
4380
|
+
// Y la foto nueva es la que dice si el paso quedó hecho, no el código de salida. Instalar
|
|
4381
|
+
// ngrok no cambia los aparatos: su foto se queda, y solo se vuelve a medir ngrok.
|
|
4382
|
+
if (receta === "ngrok") {
|
|
4383
|
+
await medirNgrokYAnunciar().catch(contar);
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4210
4386
|
informeDeDispositivos = undefined;
|
|
4211
4387
|
await atenderDispositivos().catch(contar);
|
|
4212
4388
|
})();
|
|
@@ -4229,6 +4405,9 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4229
4405
|
trabajo?.cancelar();
|
|
4230
4406
|
return;
|
|
4231
4407
|
}
|
|
4408
|
+
// La receta de ngrok es de la sesión remota: con el interruptor apagado no existe.
|
|
4409
|
+
if (mensaje.id === "ngrok" && !remotoHabilitado())
|
|
4410
|
+
return;
|
|
4232
4411
|
if (trabajo !== undefined) {
|
|
4233
4412
|
// Ya hay uno: se reenvía su estado en vez de lanzar otro.
|
|
4234
4413
|
emitirProgreso("corriendo");
|
|
@@ -4646,6 +4825,53 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4646
4825
|
.then(() => anunciarAlta())
|
|
4647
4826
|
.catch(contar);
|
|
4648
4827
|
});
|
|
4828
|
+
/**
|
|
4829
|
+
* Engancha un cliente del cable: una pestaña SSE o un MÓVIL de la sesión remota
|
|
4830
|
+
* (`puenteRemoto.ts`). Es la misma puerta para los dos a propósito: así el móvil recibe la
|
|
4831
|
+
* misma ráfaga, se muda con el foco y cuenta como «hay alguien delante» igual que una pestaña.
|
|
4832
|
+
*/
|
|
4833
|
+
const engancharCliente = (sumidero) => {
|
|
4834
|
+
clientes.add(sumidero);
|
|
4835
|
+
adjuntar(sumidero);
|
|
4836
|
+
// ANTES de `conducirCuenta()`, no después: el nombre ya está resuelto (es local, no
|
|
4837
|
+
// depende de ninguna cuenta) y el paso de cuenta puede tardar lo que tarde un humano
|
|
4838
|
+
// en elegir modelo y teclear una clave. Mandarlo solo dentro de `alta` —al final de
|
|
4839
|
+
// TODO esto— dejaba el saludo en «Hola» a secas mientras tanto (`transporte.ts`
|
|
4840
|
+
// documenta la medida).
|
|
4841
|
+
// Solo al recién llegado: los demás ya recibieron su saludo al conectar.
|
|
4842
|
+
sumidero({ clase: "bienvenida", ...(vestibulo.nombre === undefined ? {} : { nombre: vestibulo.nombre }) });
|
|
4843
|
+
};
|
|
4844
|
+
/**
|
|
4845
|
+
* Lo contrario, con la regla de «el ÚLTIMO en irse deja la consola sola» (ver el `close` de la
|
|
4846
|
+
* ruta SSE).
|
|
4847
|
+
*
|
|
4848
|
+
* Se va ESTE cliente, no «el cliente». La guarda de antes (`enviar !== sumidero`)
|
|
4849
|
+
* existía porque el `close` de una pestaña recargada puede llegar DESPUÉS de que el
|
|
4850
|
+
* SSE nuevo se enganche, y con una sola ranura eso desconectaba al recién llegado;
|
|
4851
|
+
* con un conjunto, quitar el suyo es exacto y esa carrera desaparece.
|
|
4852
|
+
*/
|
|
4853
|
+
const soltarCliente = (sumidero) => {
|
|
4854
|
+
clientes.delete(sumidero);
|
|
4855
|
+
// La consola solo se da por sola cuando se va el ÚLTIMO: el transporte lo decide
|
|
4856
|
+
// mirando sus sumideros. Cortar a la primera baja rechazaría la aprobación que otra
|
|
4857
|
+
// pestaña todavía tiene delante.
|
|
4858
|
+
adjunto?.desconectar(sumidero);
|
|
4859
|
+
if (clientes.size === 0) {
|
|
4860
|
+
/**
|
|
4861
|
+
* Y las de SEGUNDO PLANO también, que es lo que `soltar` deja pendiente a
|
|
4862
|
+
* propósito: mudarse de consola no da por ido al humano, pero cerrarse el último
|
|
4863
|
+
* SSE sí. Sin esto, una consola que se quedó detrás con un turno en marcha seguiría
|
|
4864
|
+
* creyendo que hay alguien a quien preguntar y su aprobación esperaría el plazo
|
|
4865
|
+
* entero antes de rechazarse. Se les dice a todas menos a la que ya se acaba de
|
|
4866
|
+
* cortar arriba.
|
|
4867
|
+
*/
|
|
4868
|
+
for (const consola of vestibulo.proyectosAbiertos()) {
|
|
4869
|
+
if (consola !== adjunto)
|
|
4870
|
+
consola.desconectar();
|
|
4871
|
+
}
|
|
4872
|
+
adjunto = undefined;
|
|
4873
|
+
}
|
|
4874
|
+
};
|
|
4649
4875
|
servidor.registrarRuta("GET", RUTA_EVENTOS, (peticion, respuesta) => {
|
|
4650
4876
|
respuesta.writeHead(200, {
|
|
4651
4877
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
@@ -4664,7 +4890,6 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4664
4890
|
/* el cliente se fue; el `close` de abajo ya desconecta */
|
|
4665
4891
|
}
|
|
4666
4892
|
};
|
|
4667
|
-
clientes.add(sumidero);
|
|
4668
4893
|
/**
|
|
4669
4894
|
* El identificador que el navegador eligió para ESTA conexión, si lo mandó. Es lo que
|
|
4670
4895
|
* después le permite decir «engánchame a la tarea t1» por `POST /accion`, que es otra
|
|
@@ -4700,14 +4925,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4700
4925
|
// Un comentario SSE abre el stream de verdad: sin nada escrito, algunos navegadores no
|
|
4701
4926
|
// disparan `onopen` hasta el primer dato.
|
|
4702
4927
|
respuesta.write(": xonecode\n\n");
|
|
4703
|
-
|
|
4704
|
-
// ANTES de `conducirCuenta()`, no después: el nombre ya está resuelto (es local, no
|
|
4705
|
-
// depende de ninguna cuenta) y el paso de cuenta puede tardar lo que tarde un humano
|
|
4706
|
-
// en elegir modelo y teclear una clave. Mandarlo solo dentro de `alta` —al final de
|
|
4707
|
-
// TODO esto— dejaba el saludo en «Hola» a secas mientras tanto (`transporte.ts`
|
|
4708
|
-
// documenta la medida).
|
|
4709
|
-
// Solo al recién llegado: los demás ya recibieron su saludo al conectar.
|
|
4710
|
-
sumidero({ clase: "bienvenida", ...(vestibulo.nombre === undefined ? {} : { nombre: vestibulo.nombre }) });
|
|
4928
|
+
engancharCliente(sumidero);
|
|
4711
4929
|
/**
|
|
4712
4930
|
* **Dos anuncios, y el primero va ANTES de preguntar a CloudStudio.**
|
|
4713
4931
|
*
|
|
@@ -4751,11 +4969,6 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4751
4969
|
void anunciarAlta().catch(contar);
|
|
4752
4970
|
});
|
|
4753
4971
|
peticion.on("close", () => {
|
|
4754
|
-
// Se va ESTE cliente, no «el cliente». La guarda de antes (`enviar !== sumidero`)
|
|
4755
|
-
// existía porque el `close` de una pestaña recargada puede llegar DESPUÉS de que el
|
|
4756
|
-
// SSE nuevo se enganche, y con una sola ranura eso desconectaba al recién llegado;
|
|
4757
|
-
// con un conjunto, quitar el suyo es exacto y esa carrera desaparece.
|
|
4758
|
-
clientes.delete(sumidero);
|
|
4759
4972
|
/**
|
|
4760
4973
|
* Y se desenganchan sus MIRADAS. Sin esto, el envoltorio de una pestaña cerrada se
|
|
4761
4974
|
* queda enganchado al turno de la tarea para siempre, escribiendo en un socket que ya
|
|
@@ -4773,25 +4986,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4773
4986
|
porIdDeCliente.delete(idDeCliente);
|
|
4774
4987
|
}
|
|
4775
4988
|
}
|
|
4776
|
-
|
|
4777
|
-
// mirando sus sumideros. Cortar a la primera baja rechazaría la aprobación que otra
|
|
4778
|
-
// pestaña todavía tiene delante.
|
|
4779
|
-
adjunto?.desconectar(sumidero);
|
|
4780
|
-
if (clientes.size === 0) {
|
|
4781
|
-
/**
|
|
4782
|
-
* Y las de SEGUNDO PLANO también, que es lo que `soltar` deja pendiente a
|
|
4783
|
-
* propósito: mudarse de consola no da por ido al humano, pero cerrarse el último
|
|
4784
|
-
* SSE sí. Sin esto, una consola que se quedó detrás con un turno en marcha seguiría
|
|
4785
|
-
* creyendo que hay alguien a quien preguntar y su aprobación esperaría el plazo
|
|
4786
|
-
* entero antes de rechazarse. Se les dice a todas menos a la que ya se acaba de
|
|
4787
|
-
* cortar arriba.
|
|
4788
|
-
*/
|
|
4789
|
-
for (const consola of vestibulo.proyectosAbiertos()) {
|
|
4790
|
-
if (consola !== adjunto)
|
|
4791
|
-
consola.desconectar();
|
|
4792
|
-
}
|
|
4793
|
-
adjunto = undefined;
|
|
4794
|
-
}
|
|
4989
|
+
soltarCliente(sumidero);
|
|
4795
4990
|
});
|
|
4796
4991
|
});
|
|
4797
4992
|
/**
|
|
@@ -5315,6 +5510,306 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
5315
5510
|
respuesta.writeHead(204);
|
|
5316
5511
|
respuesta.end();
|
|
5317
5512
|
});
|
|
5513
|
+
/**
|
|
5514
|
+
* Parar ESTE turno, no cerrar la conversación. Sin proyecto abierto no hay turno que
|
|
5515
|
+
* parar y se dice: un botón que no puede cumplir no puede callar. Lo usan `POST /accion`
|
|
5516
|
+
* y el móvil.
|
|
5517
|
+
*/
|
|
5518
|
+
const cancelarTurno = () => {
|
|
5519
|
+
const abierto = vestibulo.proyectoAbierto();
|
|
5520
|
+
if (abierto === undefined || !abierto.cancelarTurno()) {
|
|
5521
|
+
informar("no hay ningún turno en vuelo que parar");
|
|
5522
|
+
}
|
|
5523
|
+
};
|
|
5524
|
+
/**
|
|
5525
|
+
* Lo que va a la consola en foco: prosa, respuestas, decisiones… Lo usan `POST /accion` y
|
|
5526
|
+
* el móvil.
|
|
5527
|
+
*
|
|
5528
|
+
* Y si fue una PROSA, se reanuncia el alta: la sesión acaba de darse de alta en el
|
|
5529
|
+
* índice (`ConsolaDeProyecto.recibir`), así que hay una fila nueva que la barra tiene
|
|
5530
|
+
* que enseñar YA — con su título y marcada como la activa.
|
|
5531
|
+
*
|
|
5532
|
+
* Diferido por lo mismo que el reanuncio de los flancos: el lazo coge la línea en una
|
|
5533
|
+
* microtarea, y anunciar en el acto contaría el turno como no empezado. No sustituye a
|
|
5534
|
+
* los flancos —de ellos sale la marca de «trabajando»—, cubre el caso en que no hay
|
|
5535
|
+
* flanco ninguno: un `/comando`, que no corre turno.
|
|
5536
|
+
*/
|
|
5537
|
+
const recibirEnLaConsola = (mensaje) => {
|
|
5538
|
+
destinoActual().recibir(mensaje);
|
|
5539
|
+
if (mensaje.clase === "prosa") {
|
|
5540
|
+
void Promise.resolve()
|
|
5541
|
+
.then(() => anunciarAlta())
|
|
5542
|
+
.catch(contar);
|
|
5543
|
+
}
|
|
5544
|
+
};
|
|
5545
|
+
/**
|
|
5546
|
+
* La puerta de un MÓVIL: la de una pestaña y, detrás, el `alta` SOLO para él. Una pestaña lo
|
|
5547
|
+
* recibe porque la ruta SSE anuncia después de enganchar (`conducirCuenta().then(anunciarAlta)`);
|
|
5548
|
+
* un móvil no pasa por esa ruta, y sin esto su cabecera decía «sin sesión» hasta el siguiente
|
|
5549
|
+
* `alta` difundido. No va en `adjuntar` porque las pestañas recibirían uno de más, ni se difunde
|
|
5550
|
+
* a todos por lo mismo. Sale por su sumidero, así que cruza transformado por `filtrarSalida`
|
|
5551
|
+
* (`remoto.estado`: proyecto y título de la sesión). Se compone asíncrono: si el móvil se fue
|
|
5552
|
+
* entre medias, no se le escribe.
|
|
5553
|
+
*/
|
|
5554
|
+
const engancharMovil = (sumidero) => {
|
|
5555
|
+
engancharCliente(sumidero);
|
|
5556
|
+
void componerAlta()
|
|
5557
|
+
.then((alta) => {
|
|
5558
|
+
if (clientes.has(sumidero))
|
|
5559
|
+
sumidero(alta);
|
|
5560
|
+
})
|
|
5561
|
+
.catch(contar);
|
|
5562
|
+
};
|
|
5563
|
+
/**
|
|
5564
|
+
* Una decisión del PANEL del móvil: va a la consola de ESE id y nunca a la del foco. Si el id no
|
|
5565
|
+
* resuelve, o esa consola no tiene aprobación en vuelo, o la decisión nombra ids que no son los
|
|
5566
|
+
* de la aprobación que hay AHORA (el panel pudo quedarse viejo), se descarta y se dice: en
|
|
5567
|
+
* `consolaWeb#recibir` una decisión con ids que no casan RECHAZA lo pendiente, y aquí no se
|
|
5568
|
+
* rechaza nada que nadie haya visto.
|
|
5569
|
+
*/
|
|
5570
|
+
const decidirDesdeElPanel = (id, decisiones) => {
|
|
5571
|
+
const consola = consolaDelPanel(id);
|
|
5572
|
+
const enVuelo = consola === undefined ? undefined : aprobacionEnVueloDe(consola);
|
|
5573
|
+
const ids = Object.keys(decisiones);
|
|
5574
|
+
if (consola === undefined ||
|
|
5575
|
+
enVuelo === undefined ||
|
|
5576
|
+
ids.length === 0 ||
|
|
5577
|
+
!ids.every((nombrado) => enVuelo.pendientes.some((p) => p.id === nombrado))) {
|
|
5578
|
+
informar("sesión remota: una decisión del panel no tenía aprobación a la que contestar");
|
|
5579
|
+
return;
|
|
5580
|
+
}
|
|
5581
|
+
consola.recibir({ clase: "decision", decisiones });
|
|
5582
|
+
};
|
|
5583
|
+
/**
|
|
5584
|
+
* El móvil entra por la MISMA puerta que una pestaña (`engancharCliente`) y lo que manda va a
|
|
5585
|
+
* la consola por el MISMO despacho que `POST /accion` (`cancelarTurno`/`recibirEnLaConsola`):
|
|
5586
|
+
* un segundo camino sería una segunda regla de qué se puede hacer desde fuera.
|
|
5587
|
+
*/
|
|
5588
|
+
sesionRemota =
|
|
5589
|
+
opciones.remoto === undefined
|
|
5590
|
+
? undefined
|
|
5591
|
+
: crearSesionRemota({
|
|
5592
|
+
puerto: opciones.remoto.puerto,
|
|
5593
|
+
servidor: () => opciones.remoto.ajustes().servidor,
|
|
5594
|
+
// La vía se lee en CADA encendido (vive en `settings.json`), y el dominio con ella.
|
|
5595
|
+
tunel: () => {
|
|
5596
|
+
const { via, dominio } = opciones.remoto.ajustes();
|
|
5597
|
+
if (via !== "ngrok")
|
|
5598
|
+
return undefined;
|
|
5599
|
+
const tunel = opciones.remoto.tunel;
|
|
5600
|
+
return {
|
|
5601
|
+
abrir: () => tunel === undefined
|
|
5602
|
+
? Promise.reject(new Error("esta ejecución no tiene túnel"))
|
|
5603
|
+
: tunel.abrir(dominio === undefined ? {} : { dominio }),
|
|
5604
|
+
};
|
|
5605
|
+
},
|
|
5606
|
+
engancharCliente: engancharMovil,
|
|
5607
|
+
soltarCliente,
|
|
5608
|
+
// La `respuesta` es la excepción, y por SEGURIDAD: por `recibir` contestaría también la
|
|
5609
|
+
// subida con casillas o «¿Vaciar la copia…?» del escritorio, que el móvil nunca ve.
|
|
5610
|
+
despachar: (m) => {
|
|
5611
|
+
if (m.clase === "cancelar")
|
|
5612
|
+
cancelarTurno();
|
|
5613
|
+
else if (m.clase === "respuesta") {
|
|
5614
|
+
if (!destinoActual().responderDesdeElMovil(m.texto))
|
|
5615
|
+
informar("sesión remota: una respuesta del móvil no tenía pregunta a la que contestar");
|
|
5616
|
+
}
|
|
5617
|
+
else if (m.clase === "decision" && m.consola !== undefined)
|
|
5618
|
+
decidirDesdeElPanel(m.consola, m.decisiones);
|
|
5619
|
+
else if (m.clase === "feedback" || m.clase === "continuar") {
|
|
5620
|
+
// Solo a una tarea que ESPERA feedback: el panel la enseñó así, pero pudo cambiar entre medias.
|
|
5621
|
+
if (!tareaEsperandoFeedback(m.tarea))
|
|
5622
|
+
informar("sesión remota: el panel mandó algo a una tarea que ya no espera feedback");
|
|
5623
|
+
else if (m.clase === "continuar")
|
|
5624
|
+
void continuarTarea(m.tarea);
|
|
5625
|
+
else
|
|
5626
|
+
darFeedbackATarea(m.tarea, m.texto);
|
|
5627
|
+
}
|
|
5628
|
+
else
|
|
5629
|
+
recibirEnLaConsola(m);
|
|
5630
|
+
},
|
|
5631
|
+
alEntrarMovil: (enviar) => {
|
|
5632
|
+
try {
|
|
5633
|
+
enviar(panoramaAhora());
|
|
5634
|
+
}
|
|
5635
|
+
catch (error) {
|
|
5636
|
+
contar(error);
|
|
5637
|
+
}
|
|
5638
|
+
},
|
|
5639
|
+
informar,
|
|
5640
|
+
// A las pestañas: el diálogo de la consola. `clientes` contiene también los sumideros
|
|
5641
|
+
// de los móviles, y no pasa nada: su filtro descarta `remoto`.
|
|
5642
|
+
alCambiar: (e) => {
|
|
5643
|
+
// Los ids del panel son de UN encendido (revocar incluido): al abrir, apagar o caer se olvidan.
|
|
5644
|
+
if (e.estado === "apagada" || e.estado === "error" || e.estado === "abriendo")
|
|
5645
|
+
olvidarPanorama();
|
|
5646
|
+
for (const cliente of clientes)
|
|
5647
|
+
cliente(mensajeDeRemoto(e));
|
|
5648
|
+
},
|
|
5649
|
+
});
|
|
5650
|
+
/**
|
|
5651
|
+
* Apagar solo si hay algo ENCENDIDO. `apagar()` anuncia `{estado:"apagada"}` a todas las
|
|
5652
|
+
* pestañas, y `arrancarConsolaWeb` monta la opción SIEMPRE: con el interruptor apagado, un
|
|
5653
|
+
* apagado a ciegas en el cierre emitiría un mensaje `remoto` que no debe existir. Y no mira el
|
|
5654
|
+
* interruptor: si alguien lo apaga en `settings.json` con el puente abierto, cerrarlo sigue
|
|
5655
|
+
* siendo posible (y es lo único seguro).
|
|
5656
|
+
*/
|
|
5657
|
+
const apagarRemoto = () => {
|
|
5658
|
+
if (sesionRemota !== undefined && sesionRemota.estado().estado !== "apagada")
|
|
5659
|
+
return sesionRemota.apagar();
|
|
5660
|
+
return Promise.resolve();
|
|
5661
|
+
};
|
|
5662
|
+
/** El estado de la sesión remota, otra vez, a cada pestaña: tras cambiar algo que el mensaje lleva. */
|
|
5663
|
+
const anunciarRemoto = () => {
|
|
5664
|
+
if (sesionRemota === undefined)
|
|
5665
|
+
return;
|
|
5666
|
+
const mensaje = mensajeDeRemoto(sesionRemota.estado());
|
|
5667
|
+
for (const cliente of clientes)
|
|
5668
|
+
cliente(mensaje);
|
|
5669
|
+
};
|
|
5670
|
+
/**
|
|
5671
|
+
* Una medida a la vez, y la pedida con otra en vuelo se ENCADENA detrás (una sola, compartida
|
|
5672
|
+
* por quien la pida mientras espera): la que está en vuelo pudo empezar ANTES de guardar el
|
|
5673
|
+
* authtoken o de acabar la receta, y reutilizarla dejaría una foto vieja.
|
|
5674
|
+
*/
|
|
5675
|
+
let midiendoNgrok;
|
|
5676
|
+
let siguienteMedidaDeNgrok;
|
|
5677
|
+
const medirNgrokAhora = (medir) => {
|
|
5678
|
+
const esta = medir()
|
|
5679
|
+
.then((foto) => {
|
|
5680
|
+
fotoDeNgrok = { instalado: foto.instalado, cuenta: foto.cuenta, ...(foto.hayBrew === undefined ? {} : { hayBrew: foto.hayBrew }) };
|
|
5681
|
+
anunciarRemoto();
|
|
5682
|
+
})
|
|
5683
|
+
.finally(() => {
|
|
5684
|
+
if (midiendoNgrok === esta)
|
|
5685
|
+
midiendoNgrok = undefined;
|
|
5686
|
+
});
|
|
5687
|
+
midiendoNgrok = esta;
|
|
5688
|
+
return esta;
|
|
5689
|
+
};
|
|
5690
|
+
const medirNgrokYAnunciar = () => {
|
|
5691
|
+
const medir = opciones.remoto?.medirNgrok;
|
|
5692
|
+
if (medir === undefined || !remotoHabilitado())
|
|
5693
|
+
return Promise.resolve();
|
|
5694
|
+
const enVuelo = midiendoNgrok;
|
|
5695
|
+
if (enVuelo === undefined)
|
|
5696
|
+
return medirNgrokAhora(medir);
|
|
5697
|
+
siguienteMedidaDeNgrok ??= enVuelo
|
|
5698
|
+
.catch(() => { })
|
|
5699
|
+
.then(() => {
|
|
5700
|
+
siguienteMedidaDeNgrok = undefined;
|
|
5701
|
+
return medirNgrokAhora(medir);
|
|
5702
|
+
});
|
|
5703
|
+
return siguienteMedidaDeNgrok;
|
|
5704
|
+
};
|
|
5705
|
+
/**
|
|
5706
|
+
* Las acciones de la vía ngrok. Lo que se dice de vuelta es SIEMPRE texto nuestro: ni lo que
|
|
5707
|
+
* llegó por el cable, ni lo que contestó ngrok, ni —sobre todo— el authtoken.
|
|
5708
|
+
*/
|
|
5709
|
+
const atenderViaRemota = (via) => {
|
|
5710
|
+
if (via !== "servidor" && via !== "ngrok")
|
|
5711
|
+
return;
|
|
5712
|
+
const estado = sesionRemota?.estado().estado;
|
|
5713
|
+
// La vía se lee al encender: cambiarla encendida no haría nada hasta apagar, y un revocar
|
|
5714
|
+
// la aplicaría a medias. El diálogo solo la ofrece apagada o en error.
|
|
5715
|
+
if (estado !== "apagada" && estado !== "error") {
|
|
5716
|
+
informar("sesión remota: la vía se cambia con la sesión apagada");
|
|
5717
|
+
return;
|
|
5718
|
+
}
|
|
5719
|
+
if (!guardarAjustesDeRemoto({ via }))
|
|
5720
|
+
return;
|
|
5721
|
+
motivoDeNgrok = undefined;
|
|
5722
|
+
anunciarRemoto();
|
|
5723
|
+
void medirNgrokYAnunciar().catch(contar);
|
|
5724
|
+
};
|
|
5725
|
+
const atenderDominioRemoto = (bruto) => {
|
|
5726
|
+
if (typeof bruto !== "string")
|
|
5727
|
+
return;
|
|
5728
|
+
const dominio = bruto.trim();
|
|
5729
|
+
if (dominio !== "") {
|
|
5730
|
+
const motivo = motivoDeDominioInaceptable(dominio);
|
|
5731
|
+
if (motivo !== undefined) {
|
|
5732
|
+
// El texto del rechazo es fijo (`core/tunel.ts`): no repite lo tecleado.
|
|
5733
|
+
informar(`sesión remota: ${motivo}`);
|
|
5734
|
+
motivoDeNgrok = motivo;
|
|
5735
|
+
anunciarRemoto();
|
|
5736
|
+
if (fotoDeNgrok === undefined)
|
|
5737
|
+
void medirNgrokYAnunciar().catch(contar);
|
|
5738
|
+
return;
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
// Vacío QUITA el dominio: sin esto, uno puesto no se podría borrar desde el diálogo.
|
|
5742
|
+
if (!guardarAjustesDeRemoto({ dominio: dominio === "" ? null : dominio }))
|
|
5743
|
+
return;
|
|
5744
|
+
motivoDeNgrok = undefined;
|
|
5745
|
+
anunciarRemoto();
|
|
5746
|
+
void medirNgrokYAnunciar().catch(contar);
|
|
5747
|
+
};
|
|
5748
|
+
/** `true` si quedó escrito. El error de disco lleva la ruta de `settings.json`: solo su `code`. */
|
|
5749
|
+
const guardarAjustesDeRemoto = (cambio) => {
|
|
5750
|
+
const guardar = opciones.remoto?.guardarAjustes;
|
|
5751
|
+
if (guardar === undefined) {
|
|
5752
|
+
informar("sesión remota: esta ejecución no puede guardar sus ajustes");
|
|
5753
|
+
return false;
|
|
5754
|
+
}
|
|
5755
|
+
try {
|
|
5756
|
+
guardar(cambio);
|
|
5757
|
+
return true;
|
|
5758
|
+
}
|
|
5759
|
+
catch (error) {
|
|
5760
|
+
informar(`sesión remota: no se pudo guardar en settings.json (${codigoDe(error)})`);
|
|
5761
|
+
return false;
|
|
5762
|
+
}
|
|
5763
|
+
};
|
|
5764
|
+
/**
|
|
5765
|
+
* El interruptor de Ajustes → Sesión remota. Desactivar APAGA antes lo que haya encendido: un
|
|
5766
|
+
* puente abierto con el interruptor quitado seguiría dando la consola a quien tenga la URL.
|
|
5767
|
+
* Activar no enciende nada: solo hace aparecer el botón del chat y mide ngrok.
|
|
5768
|
+
*/
|
|
5769
|
+
const atenderHabilitarRemoto = async (valor) => {
|
|
5770
|
+
if (typeof valor !== "boolean")
|
|
5771
|
+
return;
|
|
5772
|
+
if (!valor)
|
|
5773
|
+
await apagarRemoto();
|
|
5774
|
+
if (!guardarAjustesDeRemoto({ habilitado: valor }))
|
|
5775
|
+
return;
|
|
5776
|
+
if (!valor && opciones.remoto?.ajustes().porEntorno === true) {
|
|
5777
|
+
informar("sesión remota: XONECODE_REMOTO=1 la mantiene activa en este arranque");
|
|
5778
|
+
}
|
|
5779
|
+
anunciarRemoto();
|
|
5780
|
+
if (valor)
|
|
5781
|
+
await medirNgrokYAnunciar();
|
|
5782
|
+
};
|
|
5783
|
+
/**
|
|
5784
|
+
* El authtoken entra por `leerSecreto` —la MISMA puerta que una clave de API— y sale solo hacia
|
|
5785
|
+
* `ngrok config add-authtoken`. Vacío es cancelar: no se llama a nadie ni se dice nada.
|
|
5786
|
+
*/
|
|
5787
|
+
const pedirAuthtoken = async () => {
|
|
5788
|
+
const guardar = opciones.remoto?.guardarAuthtoken;
|
|
5789
|
+
if (guardar === undefined) {
|
|
5790
|
+
informar("sesión remota: esta ejecución no puede guardar el authtoken de ngrok");
|
|
5791
|
+
return;
|
|
5792
|
+
}
|
|
5793
|
+
const consola = vestibulo.proyectoAbierto()?.consola.consola ?? vestibulo.consola.consola;
|
|
5794
|
+
const token = (await consola.leerSecreto("authtoken de ngrok: ")).trim();
|
|
5795
|
+
if (token === "")
|
|
5796
|
+
return;
|
|
5797
|
+
let resultado;
|
|
5798
|
+
try {
|
|
5799
|
+
resultado = await guardar(token);
|
|
5800
|
+
}
|
|
5801
|
+
catch {
|
|
5802
|
+
resultado = { ok: false, motivo: "" };
|
|
5803
|
+
}
|
|
5804
|
+
if (resultado.ok)
|
|
5805
|
+
motivoDeNgrok = undefined;
|
|
5806
|
+
else {
|
|
5807
|
+
motivoDeNgrok = motivoFijoDelTunel(resultado.motivo);
|
|
5808
|
+
informar(`sesión remota: ${motivoDeNgrok}`);
|
|
5809
|
+
}
|
|
5810
|
+
// La medida dice si quedó con cuenta, no lo que contestó el comando.
|
|
5811
|
+
await medirNgrokYAnunciar();
|
|
5812
|
+
};
|
|
5318
5813
|
servidor.registrarRuta("POST", RUTA_ACCION, async (peticion, respuesta) => {
|
|
5319
5814
|
let mensaje;
|
|
5320
5815
|
try {
|
|
@@ -5795,16 +6290,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
5795
6290
|
return;
|
|
5796
6291
|
}
|
|
5797
6292
|
if (mensaje.accion === "continuar" && opciones.colaDeTareas !== undefined) {
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
if (actual === undefined || actual.estado !== "requiere-atencion") {
|
|
6293
|
+
const motivo = continuarTarea(mensaje.id);
|
|
6294
|
+
if (motivo !== undefined) {
|
|
5801
6295
|
respuesta.writeHead(409, { "content-type": "application/json" });
|
|
5802
|
-
respuesta.end(JSON.stringify({ motivo
|
|
6296
|
+
respuesta.end(JSON.stringify({ motivo }));
|
|
5803
6297
|
return;
|
|
5804
6298
|
}
|
|
5805
|
-
atenderFeedbackDeTarea(mensaje.id, TEXTO_DE_CONTINUAR);
|
|
5806
|
-
opciones.revisarTareas?.();
|
|
5807
|
-
emitirTareas();
|
|
5808
6299
|
respuesta.writeHead(204);
|
|
5809
6300
|
respuesta.end();
|
|
5810
6301
|
return;
|
|
@@ -5874,9 +6365,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
5874
6365
|
void atenderAugmentar(opciones.augmentar, mensaje.proyecto, mensaje.peticion, mensaje.borrador);
|
|
5875
6366
|
}
|
|
5876
6367
|
else if (mensaje.accion === "feedback" && opciones.colaDeTareas !== undefined) {
|
|
5877
|
-
|
|
5878
|
-
opciones.revisarTareas?.();
|
|
5879
|
-
emitirTareas();
|
|
6368
|
+
darFeedbackATarea(mensaje.id, mensaje.texto);
|
|
5880
6369
|
}
|
|
5881
6370
|
else if (mensaje.accion !== "crear" &&
|
|
5882
6371
|
mensaje.accion !== "augmentar" &&
|
|
@@ -6137,17 +6626,35 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
6137
6626
|
respuesta.end();
|
|
6138
6627
|
return;
|
|
6139
6628
|
}
|
|
6140
|
-
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "
|
|
6141
|
-
//
|
|
6142
|
-
//
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6629
|
+
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "remoto") {
|
|
6630
|
+
// Con el interruptor apagado se contesta igual y no se ENCIENDE nada: ni se carga el
|
|
6631
|
+
// paquete. Apagar sí se atiende siempre, pero solo actúa si hay algo encendido.
|
|
6632
|
+
if (mensaje.accion === "apagar")
|
|
6633
|
+
void apagarRemoto().catch(contar);
|
|
6634
|
+
else if (mensaje.accion === "habilitar")
|
|
6635
|
+
void atenderHabilitarRemoto(mensaje.valor).catch(contar);
|
|
6636
|
+
else if (sesionRemota !== undefined && remotoHabilitado()) {
|
|
6637
|
+
if (mensaje.accion === "encender")
|
|
6638
|
+
void sesionRemota.encender().catch(contar);
|
|
6639
|
+
else if (mensaje.accion === "revocar")
|
|
6640
|
+
void sesionRemota.revocar().catch(contar);
|
|
6641
|
+
else if (mensaje.accion === "via")
|
|
6642
|
+
atenderViaRemota(mensaje.via);
|
|
6643
|
+
else if (mensaje.accion === "dominio")
|
|
6644
|
+
atenderDominioRemoto(mensaje.dominio);
|
|
6645
|
+
else if (mensaje.accion === "authtoken")
|
|
6646
|
+
void pedirAuthtoken().catch(contar);
|
|
6146
6647
|
}
|
|
6147
6648
|
respuesta.writeHead(204);
|
|
6148
6649
|
respuesta.end();
|
|
6149
6650
|
return;
|
|
6150
6651
|
}
|
|
6652
|
+
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "cancelar") {
|
|
6653
|
+
cancelarTurno();
|
|
6654
|
+
respuesta.writeHead(204);
|
|
6655
|
+
respuesta.end();
|
|
6656
|
+
return;
|
|
6657
|
+
}
|
|
6151
6658
|
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "sesion") {
|
|
6152
6659
|
// Suelto: abrir un proyecto arranca una consola entera y el `POST` no se queda
|
|
6153
6660
|
// esperando. Lo que pase se cuenta por el cable.
|
|
@@ -6387,27 +6894,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
6387
6894
|
void atenderAlta(mensaje);
|
|
6388
6895
|
}
|
|
6389
6896
|
else {
|
|
6390
|
-
|
|
6391
|
-
/**
|
|
6392
|
-
* Y si fue una PROSA, se reanuncia el alta: la sesión acaba de darse de alta en el
|
|
6393
|
-
* índice (`ConsolaDeProyecto.recibir`), así que hay una fila nueva que la barra tiene
|
|
6394
|
-
* que enseñar YA — con su título y marcada como la activa.
|
|
6395
|
-
*
|
|
6396
|
-
* Diferido por lo mismo que el reanuncio de los flancos: el lazo coge la línea en una
|
|
6397
|
-
* microtarea, y anunciar en el acto contaría el turno como no empezado. No sustituye a
|
|
6398
|
-
* los flancos —de ellos sale la marca de «trabajando»—, cubre el caso en que no hay
|
|
6399
|
-
* flanco ninguno: un `/comando`, que no corre turno.
|
|
6400
|
-
*/
|
|
6401
|
-
if (mensaje.clase === "prosa") {
|
|
6402
|
-
void Promise.resolve()
|
|
6403
|
-
.then(() => anunciarAlta())
|
|
6404
|
-
.catch(contar);
|
|
6405
|
-
}
|
|
6897
|
+
recibirEnLaConsola(mensaje);
|
|
6406
6898
|
}
|
|
6407
6899
|
respuesta.writeHead(204);
|
|
6408
6900
|
respuesta.end();
|
|
6409
6901
|
});
|
|
6410
|
-
return { emitirTareas };
|
|
6902
|
+
return { emitirTareas, cerrarRemoto: apagarRemoto };
|
|
6411
6903
|
}
|
|
6412
6904
|
/**
|
|
6413
6905
|
* Construye el corredor de tareas y la costura que lo conecta al cable.
|
|
@@ -7341,6 +7833,34 @@ export async function arrancarConsolaWeb(opciones) {
|
|
|
7341
7833
|
}
|
|
7342
7834
|
const cable = montarRutas(servidor, vestibulo, {
|
|
7343
7835
|
informar,
|
|
7836
|
+
// El interruptor se lee de `settings.json` (o `XONECODE_REMOTO=1`) en cada uso. Sin el
|
|
7837
|
+
// paquete del puente instalado, encender dice que no está disponible.
|
|
7838
|
+
remoto: {
|
|
7839
|
+
ajustes: () => remotoEnVigor(cargarSettings().settings.remoto, process.env),
|
|
7840
|
+
puerto: opciones.puertoRemoto ??
|
|
7841
|
+
(async () => {
|
|
7842
|
+
// `import()` dinámico también aquí: con el interruptor apagado ni se carga el adaptador.
|
|
7843
|
+
const { cargarModuloRemoto, crearPuenteWebSocket } = await import("../../agent/remoto/puenteWebSocket.js");
|
|
7844
|
+
const modulo = await cargarModuloRemoto();
|
|
7845
|
+
return modulo === undefined ? undefined : crearPuenteWebSocket(modulo);
|
|
7846
|
+
}),
|
|
7847
|
+
// La vía ngrok, también por `import()` dinámico: ni el túnel ni la medida se cargan hasta
|
|
7848
|
+
// que alguien los usa, y los dos solo se usan con el interruptor encendido.
|
|
7849
|
+
tunel: opciones.tunelRemoto ?? {
|
|
7850
|
+
abrir: async (o) => {
|
|
7851
|
+
const { crearTunelNgrok } = await import("../../agent/remoto/tunelNgrok.js");
|
|
7852
|
+
return crearTunelNgrok().abrir(o);
|
|
7853
|
+
},
|
|
7854
|
+
},
|
|
7855
|
+
medirNgrok: opciones.medirNgrok ??
|
|
7856
|
+
(async () => {
|
|
7857
|
+
const ngrok = await import("../../agent/remoto/ngrokEnMaquina.js");
|
|
7858
|
+
return { ...(await ngrok.medirNgrok()), hayBrew: ngrok.hayBrew() };
|
|
7859
|
+
}),
|
|
7860
|
+
guardarAuthtoken: opciones.guardarAuthtoken ??
|
|
7861
|
+
(async (token) => (await import("../../agent/remoto/ngrokEnMaquina.js")).guardarAuthtoken(token)),
|
|
7862
|
+
guardarAjustes: (cambio) => void guardarAjustesDeRemotoEnDisco(undefined, cambio),
|
|
7863
|
+
},
|
|
7344
7864
|
// El tope de ventana, tal cual llega: es la misma función que la barra del terminal, y
|
|
7345
7865
|
// dos resoluciones serían dos porcentajes distintos para el mismo modelo.
|
|
7346
7866
|
...(opciones.topeDeContexto === undefined ? {} : { topeDeContexto: opciones.topeDeContexto }),
|
|
@@ -7559,6 +8079,11 @@ export async function arrancarConsolaWeb(opciones) {
|
|
|
7559
8079
|
* señal repetida no puede matar el cierre que sigue: ver `interrupcion.ts`.
|
|
7560
8080
|
*/
|
|
7561
8081
|
await (opciones.esperarCierre ?? (() => esperarInterrupcion({ avisar: escribir })))();
|
|
8082
|
+
// La sesión remota antes que nada: un móvil no puede seguir mandando prosa a una consola que
|
|
8083
|
+
// se está cerrando, y el socket del puente no puede quedarse abierto tras el proceso.
|
|
8084
|
+
// Se ESPERA: con la vía ngrok, cerrar mata ngrok y el puente local, y sin esperar el proceso
|
|
8085
|
+
// saldría antes y dejaría a ngrok huérfano del `SIGKILL` de rescate.
|
|
8086
|
+
await cable.cerrarRemoto().catch(() => { });
|
|
7562
8087
|
/**
|
|
7563
8088
|
* El corredor PRIMERO, y el orden es load-bearing: `parar()` corta los turnos en vuelo y
|
|
7564
8089
|
* los deja aparcados diciendo que la consola se cerró a mitad. Al revés, el
|