xonecode 0.35.0 → 0.37.0
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-DoFEF88O.js} +1 -1
- package/apps/web/dist/assets/index-DRNhZyYF.css +1 -0
- package/apps/web/dist/assets/index-WcouJw-n.js +506 -0
- package/apps/web/dist/assets/{php-DTPkbJvq.js → php-D_qKPndv.js} +1 -1
- package/apps/web/dist/assets/{ruby-D1wBOK7G.js → ruby-Dbo7pTDa.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/motores/trueforge/sesionTrueforge.js +39 -1
- package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
- package/dist/agent/remoto/ngrokEnMaquina.js +103 -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 +158 -0
- package/dist/agent/remoto/tunelNgrok.js.map +1 -0
- 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 +101 -0
- package/dist/core/tunel.js.map +1 -0
- package/dist/web/servidor/arranque.js +587 -71
- 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 +7 -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,8 @@ 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 { motivoDeDominioInaceptable } from "../../core/tunel.js";
|
|
56
57
|
import { instalarHerramientaDeDispositivos, verificarDispositivo } from "../../agent/dispositivos/dispositivosEnMaquina.js";
|
|
57
58
|
import { arrancarEmulador, pararEmulador } from "../../agent/dispositivos/arranqueDeEmulador.js";
|
|
58
59
|
import { borrarAvd, clonarAvd, correrPasoDeReceta, crearAvd } from "../../agent/dispositivos/instalacionEnMaquina.js";
|
|
@@ -72,7 +73,7 @@ import { borrarCredencial, guardarCredencial } from "../../agent/config/authEnDi
|
|
|
72
73
|
import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedoresPersonalizados, } from "../../agent/config/configEnDisco.js";
|
|
73
74
|
import { crearCheckpointerDeProyecto, mantenimientoDelCheckpointer, } from "../../agent/sesiones/checkpointer.js";
|
|
74
75
|
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";
|
|
76
|
+
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
77
|
import { dentroDelWorkspace, depuracionActiva, expandirConCasa, mismaRuta, motivoParaNoOlvidarEntorno, motivoDeNombreDeEntornoInaceptable, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
|
|
77
78
|
import { esModoDeEscritura, MODO_POR_OMISION, } from "../../core/modoDeEscritura.js";
|
|
78
79
|
import { megasDeLasMudanzas, mudarWorkspaceLegado } from "../../agent/config/mudanzaEnDisco.js";
|
|
@@ -115,6 +116,8 @@ import { baseDeWorkspaceLegada, baseDeWorkspacePorOmision, conexionDeVestibulo,
|
|
|
115
116
|
// documentado cinco veces: una composición de producción dentro de algo que los tests
|
|
116
117
|
// doblan.
|
|
117
118
|
import { filaDeTarea } from "./transporte.js";
|
|
119
|
+
import { crearSesionRemota, motivoFijoDelTunel } from "./puenteRemoto.js";
|
|
120
|
+
import { componerPanorama, INTERVALO_DE_PANORAMA_MS } from "../../core/panorama.js";
|
|
118
121
|
import { esEsfuerzo, nivelesDeEsfuerzo } from "../../core/esfuerzo.js";
|
|
119
122
|
import { CATALOGO_DE_CONECTORES, conectoresParaElAgente, definicionDelCable, RUTA_CALLBACK_MCP } from "../../core/conectores.js";
|
|
120
123
|
import { servicioDeConectoresCableado } from "../../agent/conectores/servicioDeConectores.js";
|
|
@@ -351,6 +354,43 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
351
354
|
* Emitir es escribirle a todos; el transporte hace lo mismo por su lado.
|
|
352
355
|
*/
|
|
353
356
|
const clientes = new Set();
|
|
357
|
+
/**
|
|
358
|
+
* La sesión remota, si esta ejecución la monta. Se ASIGNA más abajo, en cuanto existen
|
|
359
|
+
* `engancharCliente`/`soltarCliente`/`cancelarTurno`/`recibirEnLaConsola`, que son su puerta y
|
|
360
|
+
* su despacho; se declara aquí porque la ráfaga (`adjuntar`) también la lee.
|
|
361
|
+
*/
|
|
362
|
+
let sesionRemota;
|
|
363
|
+
const remotoHabilitado = () => opciones.remoto?.ajustes().habilitado === true;
|
|
364
|
+
/** La última foto de ngrok (`OpcionesDeMontaje.remoto.medirNgrok`); `undefined` = aún no medida. */
|
|
365
|
+
let fotoDeNgrok;
|
|
366
|
+
/** Por qué falló la última acción de ngrok: SOLO un texto fijo nuestro. Viaja en la foto. */
|
|
367
|
+
let motivoDeNgrok;
|
|
368
|
+
const mensajeDeRemoto = (e) => {
|
|
369
|
+
const ajustes = opciones.remoto?.ajustes();
|
|
370
|
+
return {
|
|
371
|
+
clase: "remoto",
|
|
372
|
+
estado: e.estado,
|
|
373
|
+
// El interruptor, para Ajustes: la sección se pinta siempre y el botón del chat solo encendido.
|
|
374
|
+
habilitado: remotoHabilitado(),
|
|
375
|
+
...(ajustes?.porEntorno === true ? { porEntorno: true } : {}),
|
|
376
|
+
...("url" in e ? { url: e.url } : {}),
|
|
377
|
+
...("moviles" in e ? { moviles: e.moviles } : {}),
|
|
378
|
+
...("motivo" in e ? { motivo: e.motivo } : {}),
|
|
379
|
+
// La vía EN VIGOR, siempre (`remotoEnVigor` ya resolvió la omisión): el cliente toma una
|
|
380
|
+
// ausente por ngrok, así que callarla con «servidor» lo pintaría como ngrok.
|
|
381
|
+
...(ajustes?.via !== undefined ? { via: ajustes.via } : {}),
|
|
382
|
+
...(fotoDeNgrok === undefined
|
|
383
|
+
? {}
|
|
384
|
+
: {
|
|
385
|
+
ngrok: {
|
|
386
|
+
instalado: fotoDeNgrok.instalado,
|
|
387
|
+
cuenta: fotoDeNgrok.cuenta,
|
|
388
|
+
...(ajustes?.dominio === undefined ? {} : { dominio: ajustes.dominio }),
|
|
389
|
+
...(motivoDeNgrok === undefined ? {} : { motivo: motivoDeNgrok }),
|
|
390
|
+
},
|
|
391
|
+
}),
|
|
392
|
+
};
|
|
393
|
+
};
|
|
354
394
|
/**
|
|
355
395
|
* Los clientes por su IDENTIFICADOR, y qué tareas está mirando cada uno.
|
|
356
396
|
*
|
|
@@ -439,6 +479,101 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
439
479
|
for (const cliente of clientes)
|
|
440
480
|
cliente(mensaje);
|
|
441
481
|
};
|
|
482
|
+
/**
|
|
483
|
+
* El PANEL de la sesión remota (`core/panorama.ts`): las consolas de persona abiertas y la cola
|
|
484
|
+
* de tareas, compuesto AQUÍ y mandado SOLO a los móviles (`SesionRemota.difundir`), nunca a las
|
|
485
|
+
* pestañas. Cada consola cruza con un id OPACO por encendido de la sesión remota —raíz → id, que
|
|
486
|
+
* se olvida al apagar—: la raíz es una ruta de la máquina (`sinRutas`) y el proyecto viaja como
|
|
487
|
+
* su `basename`. **Límite declarado**: el mismo nombre de proyecto en dos entornos se confunde.
|
|
488
|
+
*/
|
|
489
|
+
const idsDeConsola = new Map();
|
|
490
|
+
const idDeConsola = (raiz) => {
|
|
491
|
+
let id = idsDeConsola.get(raiz);
|
|
492
|
+
if (id === undefined) {
|
|
493
|
+
id = randomUUID();
|
|
494
|
+
idsDeConsola.set(raiz, id);
|
|
495
|
+
}
|
|
496
|
+
return id;
|
|
497
|
+
};
|
|
498
|
+
/** La consola de persona ABIERTA con ese id, o nada: un id de otro encendido, o de una consola
|
|
499
|
+
* que ya se cerró, no resuelve a ninguna. */
|
|
500
|
+
const consolaDelPanel = (id) => {
|
|
501
|
+
for (const [raiz, suyo] of idsDeConsola) {
|
|
502
|
+
if (suyo === id)
|
|
503
|
+
return vestibulo.proyectosAbiertos().find((c) => c.raiz === raiz);
|
|
504
|
+
}
|
|
505
|
+
return undefined;
|
|
506
|
+
};
|
|
507
|
+
const aprobacionEnVueloDe = (c) => {
|
|
508
|
+
for (const m of c.mensajesDeAprobacion())
|
|
509
|
+
if (m.clase === "aprobacion")
|
|
510
|
+
return m;
|
|
511
|
+
return undefined;
|
|
512
|
+
};
|
|
513
|
+
const panoramaAhora = () => {
|
|
514
|
+
const foco = vestibulo.proyectoAbierto();
|
|
515
|
+
const consolas = vestibulo.proyectosAbiertos().map((c) => {
|
|
516
|
+
const fila = c.sesion === undefined ? undefined : vestibulo.sesionesDe(c.raiz).find((s) => s.id === c.sesion);
|
|
517
|
+
const aprobacion = aprobacionEnVueloDe(c);
|
|
518
|
+
return {
|
|
519
|
+
id: idDeConsola(c.raiz),
|
|
520
|
+
proyecto: basename(c.raiz),
|
|
521
|
+
...(fila === undefined || fila.titulo === "" ? {} : { sesion: fila.titulo }),
|
|
522
|
+
foco: c === foco,
|
|
523
|
+
turnoEnVuelo: c.turnoEnVuelo,
|
|
524
|
+
...(aprobacion === undefined
|
|
525
|
+
? {}
|
|
526
|
+
: { aprobacion: { pendientes: aprobacion.pendientes, ficheros: aprobacion.ficheros, diffs: aprobacion.diffs } }),
|
|
527
|
+
preguntaPendiente: c.preguntaPendiente,
|
|
528
|
+
...(fila?.ultimoTurno === undefined ? {} : { ultimaActividad: fila.ultimoTurno }),
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
const tareas = (opciones.colaDeTareas?.listar() ?? []).map((t) => ({
|
|
532
|
+
id: t.id,
|
|
533
|
+
titulo: t.titulo,
|
|
534
|
+
// El NOMBRE, como en `filaDeTarea`: la raíz de la tarea no viaja.
|
|
535
|
+
proyecto: t.proyecto.nombre,
|
|
536
|
+
estado: t.estado,
|
|
537
|
+
creada: t.creada,
|
|
538
|
+
...(t.tras === undefined ? {} : { tras: t.tras }),
|
|
539
|
+
...(t.motivo === undefined ? {} : { motivo: t.motivo }),
|
|
540
|
+
...(t.acabada === undefined ? {} : { acabada: t.acabada }),
|
|
541
|
+
}));
|
|
542
|
+
return componerPanorama({ consolas, tareas });
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Un panorama nuevo a todos los móviles, como mucho uno por `INTERVALO_DE_PANORAMA_MS` y
|
|
546
|
+
* siempre DESPUÉS del último cambio. Los gatillos (`alCambiarEsperas`, `emitirTareas`) saltan a
|
|
547
|
+
* menudo y desde dentro de una resolución (una aprobación que se contesta): aquí solo se
|
|
548
|
+
* PROGRAMA, y se compone al vencer el reloj, con el estado de ese instante.
|
|
549
|
+
*/
|
|
550
|
+
let relojDePanorama;
|
|
551
|
+
let ultimoPanorama = Number.NEGATIVE_INFINITY;
|
|
552
|
+
const programarPanorama = () => {
|
|
553
|
+
if (relojDePanorama !== undefined || sesionRemota === undefined || !sesionRemota.hayMoviles())
|
|
554
|
+
return;
|
|
555
|
+
const espera = Math.max(0, ultimoPanorama + INTERVALO_DE_PANORAMA_MS - Date.now());
|
|
556
|
+
relojDePanorama = setTimeout(() => {
|
|
557
|
+
relojDePanorama = undefined;
|
|
558
|
+
if (sesionRemota === undefined || !sesionRemota.hayMoviles())
|
|
559
|
+
return;
|
|
560
|
+
ultimoPanorama = Date.now();
|
|
561
|
+
try {
|
|
562
|
+
sesionRemota.difundir(panoramaAhora());
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
contar(error);
|
|
566
|
+
}
|
|
567
|
+
}, espera);
|
|
568
|
+
relojDePanorama.unref?.();
|
|
569
|
+
};
|
|
570
|
+
const olvidarPanorama = () => {
|
|
571
|
+
if (relojDePanorama !== undefined)
|
|
572
|
+
clearTimeout(relojDePanorama);
|
|
573
|
+
relojDePanorama = undefined;
|
|
574
|
+
idsDeConsola.clear();
|
|
575
|
+
};
|
|
576
|
+
vestibulo.alCambiarEsperas(programarPanorama);
|
|
442
577
|
/**
|
|
443
578
|
* Lo que valga esa promesa, o nada si tarda demasiado. **No la cancela**: sigue viva y su
|
|
444
579
|
* valor se usará en el siguiente anuncio — lo que se acota es la ESPERA, no el trabajo.
|
|
@@ -507,7 +642,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
507
642
|
* hace que se elija TAMBIÉN el modelo. El wizard sigue sabiendo pintarlo por si otra
|
|
508
643
|
* piel se lo manda; esta no.
|
|
509
644
|
*/
|
|
510
|
-
const
|
|
645
|
+
const componerAlta = async () => {
|
|
511
646
|
const abierto = vestibulo.proyectoAbierto();
|
|
512
647
|
const proyectoAbierto = abierto !== undefined;
|
|
513
648
|
// Se lee del disco EN CADA anuncio y no se cachea al abrir: `configurarModoInicial`
|
|
@@ -570,7 +705,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
570
705
|
conIcono.add(p.id);
|
|
571
706
|
}));
|
|
572
707
|
}
|
|
573
|
-
|
|
708
|
+
return {
|
|
574
709
|
clase: "alta",
|
|
575
710
|
pasos,
|
|
576
711
|
proveedores: PROVEEDORES.map((p) => ({ id: p, nombre: nombreDeProveedor(p) })),
|
|
@@ -680,7 +815,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
680
815
|
// en cada anuncio en vez de pasarse por parámetro: los anuncios de los flancos del
|
|
681
816
|
// turno no preparan nada y tienen que decir lo que hay, no lo que había.
|
|
682
817
|
...(preparando === undefined ? {} : { preparando }),
|
|
683
|
-
}
|
|
818
|
+
};
|
|
819
|
+
};
|
|
820
|
+
/** El alta a TODOS los clientes vivos. Compuesto aparte (`componerAlta`) para poder dárselo a
|
|
821
|
+
* UNO solo: el móvil que entra por la sesión remota (ver `engancharMovil`). */
|
|
822
|
+
const anunciarAlta = async () => {
|
|
823
|
+
emitir(await componerAlta());
|
|
684
824
|
};
|
|
685
825
|
/**
|
|
686
826
|
* Los catálogos ya consultados en ESTE proceso, por proveedor. Se guardan porque cada
|
|
@@ -1169,6 +1309,16 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
1169
1309
|
cliente(depuracion);
|
|
1170
1310
|
if (modoPorDefecto !== undefined)
|
|
1171
1311
|
cliente(modoPorDefecto);
|
|
1312
|
+
// La sesión remota, SIEMPRE que esta ejecución la monta: con `habilitado`, que es lo que
|
|
1313
|
+
// pinta el interruptor de Ajustes (y, apagado, no hay botón en el chat). Un puente abierto
|
|
1314
|
+
// con el interruptor quitado a mano en `settings.json` sale igual, para poder apagarlo.
|
|
1315
|
+
// A un móvil no le llega: `remoto` no pasa `filtrarSalida` (lleva el secreto en la `url`).
|
|
1316
|
+
if (sesionRemota !== undefined)
|
|
1317
|
+
cliente(mensajeDeRemoto(sesionRemota.estado()));
|
|
1318
|
+
// La foto de ngrok, la PRIMERA vez que alguien mira: lanza `ngrok version`, que no se hace
|
|
1319
|
+
// con el interruptor apagado ni en sondeo.
|
|
1320
|
+
if (sesionRemota !== undefined && fotoDeNgrok === undefined && midiendoNgrok === undefined && remotoHabilitado())
|
|
1321
|
+
void medirNgrokYAnunciar().catch(contar);
|
|
1172
1322
|
if (conectores !== undefined)
|
|
1173
1323
|
cliente(conectores);
|
|
1174
1324
|
if (consumoDeLaSesion !== undefined) {
|
|
@@ -1436,6 +1586,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
1436
1586
|
const m = mensajeDeTareas();
|
|
1437
1587
|
if (m !== undefined)
|
|
1438
1588
|
emitir(m);
|
|
1589
|
+
programarPanorama();
|
|
1439
1590
|
};
|
|
1440
1591
|
/**
|
|
1441
1592
|
* Un id de proyecto a su tripleta completa, o nada si no se puede resolver.
|
|
@@ -1799,6 +1950,24 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
1799
1950
|
informar(`no se pudo añadir el feedback: ${resultado.motivo ?? "motivo desconocido"}`);
|
|
1800
1951
|
}
|
|
1801
1952
|
};
|
|
1953
|
+
/**
|
|
1954
|
+
* El feedback y «Continuar» de una tarea aparcada: los MISMOS para los botones del escritorio
|
|
1955
|
+
* (`POST /accion`) y para el panel del móvil, que no es un segundo camino con su propia regla.
|
|
1956
|
+
*/
|
|
1957
|
+
const tareaEsperandoFeedback = (id) => opciones.colaDeTareas?.listar().find((t) => t.id === id)?.estado === "requiere-atencion";
|
|
1958
|
+
const darFeedbackATarea = (id, texto) => {
|
|
1959
|
+
atenderFeedbackDeTarea(id, texto);
|
|
1960
|
+
opciones.revisarTareas?.();
|
|
1961
|
+
emitirTareas();
|
|
1962
|
+
};
|
|
1963
|
+
/** Lo mismo que un feedback, con el texto de «sigue» del núcleo: entra en SU hilo, no repite el
|
|
1964
|
+
* encargo. Devuelve por qué no, o nada si se hizo. */
|
|
1965
|
+
const continuarTarea = (id) => {
|
|
1966
|
+
if (!tareaEsperandoFeedback(id))
|
|
1967
|
+
return "esa tarea ya no está esperando feedback";
|
|
1968
|
+
darFeedbackATarea(id, TEXTO_DE_CONTINUAR);
|
|
1969
|
+
return undefined;
|
|
1970
|
+
};
|
|
1802
1971
|
/** Los ajustes de destino, o `{}` —que es «se miran todos»— si esta ejecución no los lee. */
|
|
1803
1972
|
const ajustesDeDispositivos = () => opciones.ajustesDeDispositivos?.() ?? {};
|
|
1804
1973
|
/**
|
|
@@ -4646,6 +4815,53 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4646
4815
|
.then(() => anunciarAlta())
|
|
4647
4816
|
.catch(contar);
|
|
4648
4817
|
});
|
|
4818
|
+
/**
|
|
4819
|
+
* Engancha un cliente del cable: una pestaña SSE o un MÓVIL de la sesión remota
|
|
4820
|
+
* (`puenteRemoto.ts`). Es la misma puerta para los dos a propósito: así el móvil recibe la
|
|
4821
|
+
* misma ráfaga, se muda con el foco y cuenta como «hay alguien delante» igual que una pestaña.
|
|
4822
|
+
*/
|
|
4823
|
+
const engancharCliente = (sumidero) => {
|
|
4824
|
+
clientes.add(sumidero);
|
|
4825
|
+
adjuntar(sumidero);
|
|
4826
|
+
// ANTES de `conducirCuenta()`, no después: el nombre ya está resuelto (es local, no
|
|
4827
|
+
// depende de ninguna cuenta) y el paso de cuenta puede tardar lo que tarde un humano
|
|
4828
|
+
// en elegir modelo y teclear una clave. Mandarlo solo dentro de `alta` —al final de
|
|
4829
|
+
// TODO esto— dejaba el saludo en «Hola» a secas mientras tanto (`transporte.ts`
|
|
4830
|
+
// documenta la medida).
|
|
4831
|
+
// Solo al recién llegado: los demás ya recibieron su saludo al conectar.
|
|
4832
|
+
sumidero({ clase: "bienvenida", ...(vestibulo.nombre === undefined ? {} : { nombre: vestibulo.nombre }) });
|
|
4833
|
+
};
|
|
4834
|
+
/**
|
|
4835
|
+
* Lo contrario, con la regla de «el ÚLTIMO en irse deja la consola sola» (ver el `close` de la
|
|
4836
|
+
* ruta SSE).
|
|
4837
|
+
*
|
|
4838
|
+
* Se va ESTE cliente, no «el cliente». La guarda de antes (`enviar !== sumidero`)
|
|
4839
|
+
* existía porque el `close` de una pestaña recargada puede llegar DESPUÉS de que el
|
|
4840
|
+
* SSE nuevo se enganche, y con una sola ranura eso desconectaba al recién llegado;
|
|
4841
|
+
* con un conjunto, quitar el suyo es exacto y esa carrera desaparece.
|
|
4842
|
+
*/
|
|
4843
|
+
const soltarCliente = (sumidero) => {
|
|
4844
|
+
clientes.delete(sumidero);
|
|
4845
|
+
// La consola solo se da por sola cuando se va el ÚLTIMO: el transporte lo decide
|
|
4846
|
+
// mirando sus sumideros. Cortar a la primera baja rechazaría la aprobación que otra
|
|
4847
|
+
// pestaña todavía tiene delante.
|
|
4848
|
+
adjunto?.desconectar(sumidero);
|
|
4849
|
+
if (clientes.size === 0) {
|
|
4850
|
+
/**
|
|
4851
|
+
* Y las de SEGUNDO PLANO también, que es lo que `soltar` deja pendiente a
|
|
4852
|
+
* propósito: mudarse de consola no da por ido al humano, pero cerrarse el último
|
|
4853
|
+
* SSE sí. Sin esto, una consola que se quedó detrás con un turno en marcha seguiría
|
|
4854
|
+
* creyendo que hay alguien a quien preguntar y su aprobación esperaría el plazo
|
|
4855
|
+
* entero antes de rechazarse. Se les dice a todas menos a la que ya se acaba de
|
|
4856
|
+
* cortar arriba.
|
|
4857
|
+
*/
|
|
4858
|
+
for (const consola of vestibulo.proyectosAbiertos()) {
|
|
4859
|
+
if (consola !== adjunto)
|
|
4860
|
+
consola.desconectar();
|
|
4861
|
+
}
|
|
4862
|
+
adjunto = undefined;
|
|
4863
|
+
}
|
|
4864
|
+
};
|
|
4649
4865
|
servidor.registrarRuta("GET", RUTA_EVENTOS, (peticion, respuesta) => {
|
|
4650
4866
|
respuesta.writeHead(200, {
|
|
4651
4867
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
@@ -4664,7 +4880,6 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4664
4880
|
/* el cliente se fue; el `close` de abajo ya desconecta */
|
|
4665
4881
|
}
|
|
4666
4882
|
};
|
|
4667
|
-
clientes.add(sumidero);
|
|
4668
4883
|
/**
|
|
4669
4884
|
* El identificador que el navegador eligió para ESTA conexión, si lo mandó. Es lo que
|
|
4670
4885
|
* después le permite decir «engánchame a la tarea t1» por `POST /accion`, que es otra
|
|
@@ -4700,14 +4915,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4700
4915
|
// Un comentario SSE abre el stream de verdad: sin nada escrito, algunos navegadores no
|
|
4701
4916
|
// disparan `onopen` hasta el primer dato.
|
|
4702
4917
|
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 }) });
|
|
4918
|
+
engancharCliente(sumidero);
|
|
4711
4919
|
/**
|
|
4712
4920
|
* **Dos anuncios, y el primero va ANTES de preguntar a CloudStudio.**
|
|
4713
4921
|
*
|
|
@@ -4751,11 +4959,6 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4751
4959
|
void anunciarAlta().catch(contar);
|
|
4752
4960
|
});
|
|
4753
4961
|
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
4962
|
/**
|
|
4760
4963
|
* Y se desenganchan sus MIRADAS. Sin esto, el envoltorio de una pestaña cerrada se
|
|
4761
4964
|
* queda enganchado al turno de la tarea para siempre, escribiendo en un socket que ya
|
|
@@ -4773,25 +4976,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
4773
4976
|
porIdDeCliente.delete(idDeCliente);
|
|
4774
4977
|
}
|
|
4775
4978
|
}
|
|
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
|
-
}
|
|
4979
|
+
soltarCliente(sumidero);
|
|
4795
4980
|
});
|
|
4796
4981
|
});
|
|
4797
4982
|
/**
|
|
@@ -5315,6 +5500,306 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
5315
5500
|
respuesta.writeHead(204);
|
|
5316
5501
|
respuesta.end();
|
|
5317
5502
|
});
|
|
5503
|
+
/**
|
|
5504
|
+
* Parar ESTE turno, no cerrar la conversación. Sin proyecto abierto no hay turno que
|
|
5505
|
+
* parar y se dice: un botón que no puede cumplir no puede callar. Lo usan `POST /accion`
|
|
5506
|
+
* y el móvil.
|
|
5507
|
+
*/
|
|
5508
|
+
const cancelarTurno = () => {
|
|
5509
|
+
const abierto = vestibulo.proyectoAbierto();
|
|
5510
|
+
if (abierto === undefined || !abierto.cancelarTurno()) {
|
|
5511
|
+
informar("no hay ningún turno en vuelo que parar");
|
|
5512
|
+
}
|
|
5513
|
+
};
|
|
5514
|
+
/**
|
|
5515
|
+
* Lo que va a la consola en foco: prosa, respuestas, decisiones… Lo usan `POST /accion` y
|
|
5516
|
+
* el móvil.
|
|
5517
|
+
*
|
|
5518
|
+
* Y si fue una PROSA, se reanuncia el alta: la sesión acaba de darse de alta en el
|
|
5519
|
+
* índice (`ConsolaDeProyecto.recibir`), así que hay una fila nueva que la barra tiene
|
|
5520
|
+
* que enseñar YA — con su título y marcada como la activa.
|
|
5521
|
+
*
|
|
5522
|
+
* Diferido por lo mismo que el reanuncio de los flancos: el lazo coge la línea en una
|
|
5523
|
+
* microtarea, y anunciar en el acto contaría el turno como no empezado. No sustituye a
|
|
5524
|
+
* los flancos —de ellos sale la marca de «trabajando»—, cubre el caso en que no hay
|
|
5525
|
+
* flanco ninguno: un `/comando`, que no corre turno.
|
|
5526
|
+
*/
|
|
5527
|
+
const recibirEnLaConsola = (mensaje) => {
|
|
5528
|
+
destinoActual().recibir(mensaje);
|
|
5529
|
+
if (mensaje.clase === "prosa") {
|
|
5530
|
+
void Promise.resolve()
|
|
5531
|
+
.then(() => anunciarAlta())
|
|
5532
|
+
.catch(contar);
|
|
5533
|
+
}
|
|
5534
|
+
};
|
|
5535
|
+
/**
|
|
5536
|
+
* La puerta de un MÓVIL: la de una pestaña y, detrás, el `alta` SOLO para él. Una pestaña lo
|
|
5537
|
+
* recibe porque la ruta SSE anuncia después de enganchar (`conducirCuenta().then(anunciarAlta)`);
|
|
5538
|
+
* un móvil no pasa por esa ruta, y sin esto su cabecera decía «sin sesión» hasta el siguiente
|
|
5539
|
+
* `alta` difundido. No va en `adjuntar` porque las pestañas recibirían uno de más, ni se difunde
|
|
5540
|
+
* a todos por lo mismo. Sale por su sumidero, así que cruza transformado por `filtrarSalida`
|
|
5541
|
+
* (`remoto.estado`: proyecto y título de la sesión). Se compone asíncrono: si el móvil se fue
|
|
5542
|
+
* entre medias, no se le escribe.
|
|
5543
|
+
*/
|
|
5544
|
+
const engancharMovil = (sumidero) => {
|
|
5545
|
+
engancharCliente(sumidero);
|
|
5546
|
+
void componerAlta()
|
|
5547
|
+
.then((alta) => {
|
|
5548
|
+
if (clientes.has(sumidero))
|
|
5549
|
+
sumidero(alta);
|
|
5550
|
+
})
|
|
5551
|
+
.catch(contar);
|
|
5552
|
+
};
|
|
5553
|
+
/**
|
|
5554
|
+
* 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
|
|
5555
|
+
* resuelve, o esa consola no tiene aprobación en vuelo, o la decisión nombra ids que no son los
|
|
5556
|
+
* de la aprobación que hay AHORA (el panel pudo quedarse viejo), se descarta y se dice: en
|
|
5557
|
+
* `consolaWeb#recibir` una decisión con ids que no casan RECHAZA lo pendiente, y aquí no se
|
|
5558
|
+
* rechaza nada que nadie haya visto.
|
|
5559
|
+
*/
|
|
5560
|
+
const decidirDesdeElPanel = (id, decisiones) => {
|
|
5561
|
+
const consola = consolaDelPanel(id);
|
|
5562
|
+
const enVuelo = consola === undefined ? undefined : aprobacionEnVueloDe(consola);
|
|
5563
|
+
const ids = Object.keys(decisiones);
|
|
5564
|
+
if (consola === undefined ||
|
|
5565
|
+
enVuelo === undefined ||
|
|
5566
|
+
ids.length === 0 ||
|
|
5567
|
+
!ids.every((nombrado) => enVuelo.pendientes.some((p) => p.id === nombrado))) {
|
|
5568
|
+
informar("sesión remota: una decisión del panel no tenía aprobación a la que contestar");
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5571
|
+
consola.recibir({ clase: "decision", decisiones });
|
|
5572
|
+
};
|
|
5573
|
+
/**
|
|
5574
|
+
* El móvil entra por la MISMA puerta que una pestaña (`engancharCliente`) y lo que manda va a
|
|
5575
|
+
* la consola por el MISMO despacho que `POST /accion` (`cancelarTurno`/`recibirEnLaConsola`):
|
|
5576
|
+
* un segundo camino sería una segunda regla de qué se puede hacer desde fuera.
|
|
5577
|
+
*/
|
|
5578
|
+
sesionRemota =
|
|
5579
|
+
opciones.remoto === undefined
|
|
5580
|
+
? undefined
|
|
5581
|
+
: crearSesionRemota({
|
|
5582
|
+
puerto: opciones.remoto.puerto,
|
|
5583
|
+
servidor: () => opciones.remoto.ajustes().servidor,
|
|
5584
|
+
// La vía se lee en CADA encendido (vive en `settings.json`), y el dominio con ella.
|
|
5585
|
+
tunel: () => {
|
|
5586
|
+
const { via, dominio } = opciones.remoto.ajustes();
|
|
5587
|
+
if (via !== "ngrok")
|
|
5588
|
+
return undefined;
|
|
5589
|
+
const tunel = opciones.remoto.tunel;
|
|
5590
|
+
return {
|
|
5591
|
+
abrir: () => tunel === undefined
|
|
5592
|
+
? Promise.reject(new Error("esta ejecución no tiene túnel"))
|
|
5593
|
+
: tunel.abrir(dominio === undefined ? {} : { dominio }),
|
|
5594
|
+
};
|
|
5595
|
+
},
|
|
5596
|
+
engancharCliente: engancharMovil,
|
|
5597
|
+
soltarCliente,
|
|
5598
|
+
// La `respuesta` es la excepción, y por SEGURIDAD: por `recibir` contestaría también la
|
|
5599
|
+
// subida con casillas o «¿Vaciar la copia…?» del escritorio, que el móvil nunca ve.
|
|
5600
|
+
despachar: (m) => {
|
|
5601
|
+
if (m.clase === "cancelar")
|
|
5602
|
+
cancelarTurno();
|
|
5603
|
+
else if (m.clase === "respuesta") {
|
|
5604
|
+
if (!destinoActual().responderDesdeElMovil(m.texto))
|
|
5605
|
+
informar("sesión remota: una respuesta del móvil no tenía pregunta a la que contestar");
|
|
5606
|
+
}
|
|
5607
|
+
else if (m.clase === "decision" && m.consola !== undefined)
|
|
5608
|
+
decidirDesdeElPanel(m.consola, m.decisiones);
|
|
5609
|
+
else if (m.clase === "feedback" || m.clase === "continuar") {
|
|
5610
|
+
// Solo a una tarea que ESPERA feedback: el panel la enseñó así, pero pudo cambiar entre medias.
|
|
5611
|
+
if (!tareaEsperandoFeedback(m.tarea))
|
|
5612
|
+
informar("sesión remota: el panel mandó algo a una tarea que ya no espera feedback");
|
|
5613
|
+
else if (m.clase === "continuar")
|
|
5614
|
+
void continuarTarea(m.tarea);
|
|
5615
|
+
else
|
|
5616
|
+
darFeedbackATarea(m.tarea, m.texto);
|
|
5617
|
+
}
|
|
5618
|
+
else
|
|
5619
|
+
recibirEnLaConsola(m);
|
|
5620
|
+
},
|
|
5621
|
+
alEntrarMovil: (enviar) => {
|
|
5622
|
+
try {
|
|
5623
|
+
enviar(panoramaAhora());
|
|
5624
|
+
}
|
|
5625
|
+
catch (error) {
|
|
5626
|
+
contar(error);
|
|
5627
|
+
}
|
|
5628
|
+
},
|
|
5629
|
+
informar,
|
|
5630
|
+
// A las pestañas: el diálogo de la consola. `clientes` contiene también los sumideros
|
|
5631
|
+
// de los móviles, y no pasa nada: su filtro descarta `remoto`.
|
|
5632
|
+
alCambiar: (e) => {
|
|
5633
|
+
// Los ids del panel son de UN encendido (revocar incluido): al abrir, apagar o caer se olvidan.
|
|
5634
|
+
if (e.estado === "apagada" || e.estado === "error" || e.estado === "abriendo")
|
|
5635
|
+
olvidarPanorama();
|
|
5636
|
+
for (const cliente of clientes)
|
|
5637
|
+
cliente(mensajeDeRemoto(e));
|
|
5638
|
+
},
|
|
5639
|
+
});
|
|
5640
|
+
/**
|
|
5641
|
+
* Apagar solo si hay algo ENCENDIDO. `apagar()` anuncia `{estado:"apagada"}` a todas las
|
|
5642
|
+
* pestañas, y `arrancarConsolaWeb` monta la opción SIEMPRE: con el interruptor apagado, un
|
|
5643
|
+
* apagado a ciegas en el cierre emitiría un mensaje `remoto` que no debe existir. Y no mira el
|
|
5644
|
+
* interruptor: si alguien lo apaga en `settings.json` con el puente abierto, cerrarlo sigue
|
|
5645
|
+
* siendo posible (y es lo único seguro).
|
|
5646
|
+
*/
|
|
5647
|
+
const apagarRemoto = () => {
|
|
5648
|
+
if (sesionRemota !== undefined && sesionRemota.estado().estado !== "apagada")
|
|
5649
|
+
return sesionRemota.apagar();
|
|
5650
|
+
return Promise.resolve();
|
|
5651
|
+
};
|
|
5652
|
+
/** El estado de la sesión remota, otra vez, a cada pestaña: tras cambiar algo que el mensaje lleva. */
|
|
5653
|
+
const anunciarRemoto = () => {
|
|
5654
|
+
if (sesionRemota === undefined)
|
|
5655
|
+
return;
|
|
5656
|
+
const mensaje = mensajeDeRemoto(sesionRemota.estado());
|
|
5657
|
+
for (const cliente of clientes)
|
|
5658
|
+
cliente(mensaje);
|
|
5659
|
+
};
|
|
5660
|
+
/**
|
|
5661
|
+
* Una medida a la vez, y la pedida con otra en vuelo se ENCADENA detrás (una sola, compartida
|
|
5662
|
+
* por quien la pida mientras espera): la que está en vuelo pudo empezar ANTES de guardar el
|
|
5663
|
+
* authtoken o de acabar la receta, y reutilizarla dejaría una foto vieja.
|
|
5664
|
+
*/
|
|
5665
|
+
let midiendoNgrok;
|
|
5666
|
+
let siguienteMedidaDeNgrok;
|
|
5667
|
+
const medirNgrokAhora = (medir) => {
|
|
5668
|
+
const esta = medir()
|
|
5669
|
+
.then((foto) => {
|
|
5670
|
+
fotoDeNgrok = { instalado: foto.instalado, cuenta: foto.cuenta };
|
|
5671
|
+
anunciarRemoto();
|
|
5672
|
+
})
|
|
5673
|
+
.finally(() => {
|
|
5674
|
+
if (midiendoNgrok === esta)
|
|
5675
|
+
midiendoNgrok = undefined;
|
|
5676
|
+
});
|
|
5677
|
+
midiendoNgrok = esta;
|
|
5678
|
+
return esta;
|
|
5679
|
+
};
|
|
5680
|
+
const medirNgrokYAnunciar = () => {
|
|
5681
|
+
const medir = opciones.remoto?.medirNgrok;
|
|
5682
|
+
if (medir === undefined || !remotoHabilitado())
|
|
5683
|
+
return Promise.resolve();
|
|
5684
|
+
const enVuelo = midiendoNgrok;
|
|
5685
|
+
if (enVuelo === undefined)
|
|
5686
|
+
return medirNgrokAhora(medir);
|
|
5687
|
+
siguienteMedidaDeNgrok ??= enVuelo
|
|
5688
|
+
.catch(() => { })
|
|
5689
|
+
.then(() => {
|
|
5690
|
+
siguienteMedidaDeNgrok = undefined;
|
|
5691
|
+
return medirNgrokAhora(medir);
|
|
5692
|
+
});
|
|
5693
|
+
return siguienteMedidaDeNgrok;
|
|
5694
|
+
};
|
|
5695
|
+
/**
|
|
5696
|
+
* Las acciones de la vía ngrok. Lo que se dice de vuelta es SIEMPRE texto nuestro: ni lo que
|
|
5697
|
+
* llegó por el cable, ni lo que contestó ngrok, ni —sobre todo— el authtoken.
|
|
5698
|
+
*/
|
|
5699
|
+
const atenderViaRemota = (via) => {
|
|
5700
|
+
if (via !== "servidor" && via !== "ngrok")
|
|
5701
|
+
return;
|
|
5702
|
+
const estado = sesionRemota?.estado().estado;
|
|
5703
|
+
// La vía se lee al encender: cambiarla encendida no haría nada hasta apagar, y un revocar
|
|
5704
|
+
// la aplicaría a medias. El diálogo solo la ofrece apagada o en error.
|
|
5705
|
+
if (estado !== "apagada" && estado !== "error") {
|
|
5706
|
+
informar("sesión remota: la vía se cambia con la sesión apagada");
|
|
5707
|
+
return;
|
|
5708
|
+
}
|
|
5709
|
+
if (!guardarAjustesDeRemoto({ via }))
|
|
5710
|
+
return;
|
|
5711
|
+
motivoDeNgrok = undefined;
|
|
5712
|
+
anunciarRemoto();
|
|
5713
|
+
void medirNgrokYAnunciar().catch(contar);
|
|
5714
|
+
};
|
|
5715
|
+
const atenderDominioRemoto = (bruto) => {
|
|
5716
|
+
if (typeof bruto !== "string")
|
|
5717
|
+
return;
|
|
5718
|
+
const dominio = bruto.trim();
|
|
5719
|
+
if (dominio !== "") {
|
|
5720
|
+
const motivo = motivoDeDominioInaceptable(dominio);
|
|
5721
|
+
if (motivo !== undefined) {
|
|
5722
|
+
// El texto del rechazo es fijo (`core/tunel.ts`): no repite lo tecleado.
|
|
5723
|
+
informar(`sesión remota: ${motivo}`);
|
|
5724
|
+
motivoDeNgrok = motivo;
|
|
5725
|
+
anunciarRemoto();
|
|
5726
|
+
if (fotoDeNgrok === undefined)
|
|
5727
|
+
void medirNgrokYAnunciar().catch(contar);
|
|
5728
|
+
return;
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
// Vacío QUITA el dominio: sin esto, uno puesto no se podría borrar desde el diálogo.
|
|
5732
|
+
if (!guardarAjustesDeRemoto({ dominio: dominio === "" ? null : dominio }))
|
|
5733
|
+
return;
|
|
5734
|
+
motivoDeNgrok = undefined;
|
|
5735
|
+
anunciarRemoto();
|
|
5736
|
+
void medirNgrokYAnunciar().catch(contar);
|
|
5737
|
+
};
|
|
5738
|
+
/** `true` si quedó escrito. El error de disco lleva la ruta de `settings.json`: solo su `code`. */
|
|
5739
|
+
const guardarAjustesDeRemoto = (cambio) => {
|
|
5740
|
+
const guardar = opciones.remoto?.guardarAjustes;
|
|
5741
|
+
if (guardar === undefined) {
|
|
5742
|
+
informar("sesión remota: esta ejecución no puede guardar sus ajustes");
|
|
5743
|
+
return false;
|
|
5744
|
+
}
|
|
5745
|
+
try {
|
|
5746
|
+
guardar(cambio);
|
|
5747
|
+
return true;
|
|
5748
|
+
}
|
|
5749
|
+
catch (error) {
|
|
5750
|
+
informar(`sesión remota: no se pudo guardar en settings.json (${codigoDe(error)})`);
|
|
5751
|
+
return false;
|
|
5752
|
+
}
|
|
5753
|
+
};
|
|
5754
|
+
/**
|
|
5755
|
+
* El interruptor de Ajustes → Sesión remota. Desactivar APAGA antes lo que haya encendido: un
|
|
5756
|
+
* puente abierto con el interruptor quitado seguiría dando la consola a quien tenga la URL.
|
|
5757
|
+
* Activar no enciende nada: solo hace aparecer el botón del chat y mide ngrok.
|
|
5758
|
+
*/
|
|
5759
|
+
const atenderHabilitarRemoto = async (valor) => {
|
|
5760
|
+
if (typeof valor !== "boolean")
|
|
5761
|
+
return;
|
|
5762
|
+
if (!valor)
|
|
5763
|
+
await apagarRemoto();
|
|
5764
|
+
if (!guardarAjustesDeRemoto({ habilitado: valor }))
|
|
5765
|
+
return;
|
|
5766
|
+
if (!valor && opciones.remoto?.ajustes().porEntorno === true) {
|
|
5767
|
+
informar("sesión remota: XONECODE_REMOTO=1 la mantiene activa en este arranque");
|
|
5768
|
+
}
|
|
5769
|
+
anunciarRemoto();
|
|
5770
|
+
if (valor)
|
|
5771
|
+
await medirNgrokYAnunciar();
|
|
5772
|
+
};
|
|
5773
|
+
/**
|
|
5774
|
+
* El authtoken entra por `leerSecreto` —la MISMA puerta que una clave de API— y sale solo hacia
|
|
5775
|
+
* `ngrok config add-authtoken`. Vacío es cancelar: no se llama a nadie ni se dice nada.
|
|
5776
|
+
*/
|
|
5777
|
+
const pedirAuthtoken = async () => {
|
|
5778
|
+
const guardar = opciones.remoto?.guardarAuthtoken;
|
|
5779
|
+
if (guardar === undefined) {
|
|
5780
|
+
informar("sesión remota: esta ejecución no puede guardar el authtoken de ngrok");
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
const consola = vestibulo.proyectoAbierto()?.consola.consola ?? vestibulo.consola.consola;
|
|
5784
|
+
const token = (await consola.leerSecreto("authtoken de ngrok: ")).trim();
|
|
5785
|
+
if (token === "")
|
|
5786
|
+
return;
|
|
5787
|
+
let resultado;
|
|
5788
|
+
try {
|
|
5789
|
+
resultado = await guardar(token);
|
|
5790
|
+
}
|
|
5791
|
+
catch {
|
|
5792
|
+
resultado = { ok: false, motivo: "" };
|
|
5793
|
+
}
|
|
5794
|
+
if (resultado.ok)
|
|
5795
|
+
motivoDeNgrok = undefined;
|
|
5796
|
+
else {
|
|
5797
|
+
motivoDeNgrok = motivoFijoDelTunel(resultado.motivo);
|
|
5798
|
+
informar(`sesión remota: ${motivoDeNgrok}`);
|
|
5799
|
+
}
|
|
5800
|
+
// La medida dice si quedó con cuenta, no lo que contestó el comando.
|
|
5801
|
+
await medirNgrokYAnunciar();
|
|
5802
|
+
};
|
|
5318
5803
|
servidor.registrarRuta("POST", RUTA_ACCION, async (peticion, respuesta) => {
|
|
5319
5804
|
let mensaje;
|
|
5320
5805
|
try {
|
|
@@ -5795,16 +6280,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
5795
6280
|
return;
|
|
5796
6281
|
}
|
|
5797
6282
|
if (mensaje.accion === "continuar" && opciones.colaDeTareas !== undefined) {
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
if (actual === undefined || actual.estado !== "requiere-atencion") {
|
|
6283
|
+
const motivo = continuarTarea(mensaje.id);
|
|
6284
|
+
if (motivo !== undefined) {
|
|
5801
6285
|
respuesta.writeHead(409, { "content-type": "application/json" });
|
|
5802
|
-
respuesta.end(JSON.stringify({ motivo
|
|
6286
|
+
respuesta.end(JSON.stringify({ motivo }));
|
|
5803
6287
|
return;
|
|
5804
6288
|
}
|
|
5805
|
-
atenderFeedbackDeTarea(mensaje.id, TEXTO_DE_CONTINUAR);
|
|
5806
|
-
opciones.revisarTareas?.();
|
|
5807
|
-
emitirTareas();
|
|
5808
6289
|
respuesta.writeHead(204);
|
|
5809
6290
|
respuesta.end();
|
|
5810
6291
|
return;
|
|
@@ -5874,9 +6355,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
5874
6355
|
void atenderAugmentar(opciones.augmentar, mensaje.proyecto, mensaje.peticion, mensaje.borrador);
|
|
5875
6356
|
}
|
|
5876
6357
|
else if (mensaje.accion === "feedback" && opciones.colaDeTareas !== undefined) {
|
|
5877
|
-
|
|
5878
|
-
opciones.revisarTareas?.();
|
|
5879
|
-
emitirTareas();
|
|
6358
|
+
darFeedbackATarea(mensaje.id, mensaje.texto);
|
|
5880
6359
|
}
|
|
5881
6360
|
else if (mensaje.accion !== "crear" &&
|
|
5882
6361
|
mensaje.accion !== "augmentar" &&
|
|
@@ -6137,17 +6616,35 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
6137
6616
|
respuesta.end();
|
|
6138
6617
|
return;
|
|
6139
6618
|
}
|
|
6140
|
-
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "
|
|
6141
|
-
//
|
|
6142
|
-
//
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6619
|
+
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "remoto") {
|
|
6620
|
+
// Con el interruptor apagado se contesta igual y no se ENCIENDE nada: ni se carga el
|
|
6621
|
+
// paquete. Apagar sí se atiende siempre, pero solo actúa si hay algo encendido.
|
|
6622
|
+
if (mensaje.accion === "apagar")
|
|
6623
|
+
void apagarRemoto().catch(contar);
|
|
6624
|
+
else if (mensaje.accion === "habilitar")
|
|
6625
|
+
void atenderHabilitarRemoto(mensaje.valor).catch(contar);
|
|
6626
|
+
else if (sesionRemota !== undefined && remotoHabilitado()) {
|
|
6627
|
+
if (mensaje.accion === "encender")
|
|
6628
|
+
void sesionRemota.encender().catch(contar);
|
|
6629
|
+
else if (mensaje.accion === "revocar")
|
|
6630
|
+
void sesionRemota.revocar().catch(contar);
|
|
6631
|
+
else if (mensaje.accion === "via")
|
|
6632
|
+
atenderViaRemota(mensaje.via);
|
|
6633
|
+
else if (mensaje.accion === "dominio")
|
|
6634
|
+
atenderDominioRemoto(mensaje.dominio);
|
|
6635
|
+
else if (mensaje.accion === "authtoken")
|
|
6636
|
+
void pedirAuthtoken().catch(contar);
|
|
6146
6637
|
}
|
|
6147
6638
|
respuesta.writeHead(204);
|
|
6148
6639
|
respuesta.end();
|
|
6149
6640
|
return;
|
|
6150
6641
|
}
|
|
6642
|
+
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "cancelar") {
|
|
6643
|
+
cancelarTurno();
|
|
6644
|
+
respuesta.writeHead(204);
|
|
6645
|
+
respuesta.end();
|
|
6646
|
+
return;
|
|
6647
|
+
}
|
|
6151
6648
|
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "sesion") {
|
|
6152
6649
|
// Suelto: abrir un proyecto arranca una consola entera y el `POST` no se queda
|
|
6153
6650
|
// esperando. Lo que pase se cuenta por el cable.
|
|
@@ -6387,27 +6884,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
6387
6884
|
void atenderAlta(mensaje);
|
|
6388
6885
|
}
|
|
6389
6886
|
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
|
-
}
|
|
6887
|
+
recibirEnLaConsola(mensaje);
|
|
6406
6888
|
}
|
|
6407
6889
|
respuesta.writeHead(204);
|
|
6408
6890
|
respuesta.end();
|
|
6409
6891
|
});
|
|
6410
|
-
return { emitirTareas };
|
|
6892
|
+
return { emitirTareas, cerrarRemoto: apagarRemoto };
|
|
6411
6893
|
}
|
|
6412
6894
|
/**
|
|
6413
6895
|
* Construye el corredor de tareas y la costura que lo conecta al cable.
|
|
@@ -7341,6 +7823,35 @@ export async function arrancarConsolaWeb(opciones) {
|
|
|
7341
7823
|
}
|
|
7342
7824
|
const cable = montarRutas(servidor, vestibulo, {
|
|
7343
7825
|
informar,
|
|
7826
|
+
// El interruptor se lee de `settings.json` (o `XONECODE_REMOTO=1`) en cada uso. Sin el
|
|
7827
|
+
// paquete del puente instalado, encender dice que no está disponible.
|
|
7828
|
+
remoto: {
|
|
7829
|
+
ajustes: () => remotoEnVigor(cargarSettings().settings.remoto, process.env),
|
|
7830
|
+
puerto: opciones.puertoRemoto ??
|
|
7831
|
+
(async () => {
|
|
7832
|
+
// `import()` dinámico también aquí: con el interruptor apagado ni se carga el adaptador.
|
|
7833
|
+
const { cargarModuloRemoto, crearPuenteWebSocket } = await import("../../agent/remoto/puenteWebSocket.js");
|
|
7834
|
+
const modulo = await cargarModuloRemoto();
|
|
7835
|
+
return modulo === undefined ? undefined : crearPuenteWebSocket(modulo);
|
|
7836
|
+
}),
|
|
7837
|
+
// La vía ngrok, también por `import()` dinámico: ni el túnel ni la medida se cargan hasta
|
|
7838
|
+
// que alguien los usa, y los dos solo se usan con el interruptor encendido.
|
|
7839
|
+
tunel: opciones.tunelRemoto ?? {
|
|
7840
|
+
abrir: async (o) => {
|
|
7841
|
+
const { crearTunelNgrok } = await import("../../agent/remoto/tunelNgrok.js");
|
|
7842
|
+
return crearTunelNgrok().abrir(o);
|
|
7843
|
+
},
|
|
7844
|
+
},
|
|
7845
|
+
medirNgrok: opciones.medirNgrok ??
|
|
7846
|
+
(async () => {
|
|
7847
|
+
// ngrok viene con xonecode (el SDK): no hay receta de instalación que medir.
|
|
7848
|
+
const ngrok = await import("../../agent/remoto/ngrokEnMaquina.js");
|
|
7849
|
+
return ngrok.medirNgrok();
|
|
7850
|
+
}),
|
|
7851
|
+
guardarAuthtoken: opciones.guardarAuthtoken ??
|
|
7852
|
+
(async (token) => (await import("../../agent/remoto/ngrokEnMaquina.js")).guardarAuthtoken(token)),
|
|
7853
|
+
guardarAjustes: (cambio) => void guardarAjustesDeRemotoEnDisco(undefined, cambio),
|
|
7854
|
+
},
|
|
7344
7855
|
// El tope de ventana, tal cual llega: es la misma función que la barra del terminal, y
|
|
7345
7856
|
// dos resoluciones serían dos porcentajes distintos para el mismo modelo.
|
|
7346
7857
|
...(opciones.topeDeContexto === undefined ? {} : { topeDeContexto: opciones.topeDeContexto }),
|
|
@@ -7559,6 +8070,11 @@ export async function arrancarConsolaWeb(opciones) {
|
|
|
7559
8070
|
* señal repetida no puede matar el cierre que sigue: ver `interrupcion.ts`.
|
|
7560
8071
|
*/
|
|
7561
8072
|
await (opciones.esperarCierre ?? (() => esperarInterrupcion({ avisar: escribir })))();
|
|
8073
|
+
// La sesión remota antes que nada: un móvil no puede seguir mandando prosa a una consola que
|
|
8074
|
+
// se está cerrando, y el socket del puente no puede quedarse abierto tras el proceso.
|
|
8075
|
+
// Se ESPERA: con la vía ngrok, cerrar mata ngrok y el puente local, y sin esperar el proceso
|
|
8076
|
+
// saldría antes y dejaría a ngrok huérfano del `SIGKILL` de rescate.
|
|
8077
|
+
await cable.cerrarRemoto().catch(() => { });
|
|
7562
8078
|
/**
|
|
7563
8079
|
* El corredor PRIMERO, y el orden es load-bearing: `parar()` corta los turnos en vuelo y
|
|
7564
8080
|
* los deja aparcados diciendo que la consola se cerró a mitad. Al revés, el
|