@danieltmn/openbridge 0.5.2 → 0.6.1
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/CHANGELOG.md +27 -0
- package/README.md +29 -2
- package/package.json +1 -1
- package/src/cli.js +178 -8
- package/src/web/assets/app.js +87 -0
- package/src/web/templates/chat.html +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ Todos los cambios relevantes de OpenBridge. Formato basado en
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y
|
|
5
5
|
[Versionado Semantico](https://semver.org/lang/es/).
|
|
6
6
|
|
|
7
|
+
## [0.6.1] - 2026-09-14
|
|
8
|
+
|
|
9
|
+
### Agregado
|
|
10
|
+
|
|
11
|
+
- `openbridge bridge --background` (segundo plano; sobrevive al cierre de la
|
|
12
|
+
consola), `--stop` y `--status`. `openbridge pair`/`join` aceptan `--background`.
|
|
13
|
+
|
|
14
|
+
### Corregido
|
|
15
|
+
|
|
16
|
+
- `autostart` corre solo el puente cuando la PC pertenece a un hub remoto
|
|
17
|
+
(`join`/`pair`), en vez del server local.
|
|
18
|
+
|
|
19
|
+
## [0.6.0] - 2026-09-14
|
|
20
|
+
|
|
21
|
+
### Agregado
|
|
22
|
+
|
|
23
|
+
- **Hub PHP** para correr la app en un hosting (cPanel) con URL fija, sin tunel
|
|
24
|
+
(`php/app` + `scripts/build-php-hub.mjs`; comparte el frontend con el hub Node).
|
|
25
|
+
Auth multiusuario con scrypt compatible con Node (usa `sodium`; si falta, scrypt
|
|
26
|
+
puro en PHP), roles, cookie de sesion firmada, rate limit y CSP.
|
|
27
|
+
- **Emparejamiento por codigo** (`openbridge pair <url>`): la PC muestra un codigo,
|
|
28
|
+
el usuario lo ingresa en la web (Dispositivos) y la PC recibe su **token propio**
|
|
29
|
+
y revocable. Cada usuario ve solo sus PCs; el admin, todas.
|
|
30
|
+
- Comando `openbridge pair` y vista **Dispositivos** en la web (solo en el hub PHP,
|
|
31
|
+
gateada por `features.pairing`).
|
|
32
|
+
- `docs/DEPLOY-PHP.md`: guia de despliegue del hub PHP.
|
|
33
|
+
|
|
7
34
|
## [0.5.2] - 2026-09-14
|
|
8
35
|
|
|
9
36
|
### Corregido
|
package/README.md
CHANGED
|
@@ -89,8 +89,9 @@ iniciar sesión** una vez; los chats, carpetas, túnel y push se conservan.
|
|
|
89
89
|
| `openbridge qr` | Muestra la URL (pública o local) como QR para escanear desde el celular |
|
|
90
90
|
| `openbridge tunnel` | Muestra o cambia el proveedor de túnel y su dominio fijo (`--domain`) |
|
|
91
91
|
| `openbridge logs` | Logs (`--follow`, `--server`, `--bridge`) |
|
|
92
|
-
| `openbridge bridge` | Corre **solo** el puente (`--
|
|
93
|
-
| `openbridge join` | Vincula esta PC como puente de un hub (`<url> --token --id --name`) |
|
|
92
|
+
| `openbridge bridge` | Corre **solo** el puente (`--background` = segundo plano, `--stop`, `--status`) |
|
|
93
|
+
| `openbridge join` | Vincula esta PC como puente de un hub (`<url> --token --id --name [--background]`) |
|
|
94
|
+
| `openbridge pair` | Empareja esta PC con un **hub PHP** por código (`<url> [--id --name] [--background]`) |
|
|
94
95
|
| `openbridge import` | Trae `data/` de OpenConex (`<data-dir> [--force]`) |
|
|
95
96
|
| `openbridge reset` | Borra chats/datos (`--session <id>`, `--yes`) |
|
|
96
97
|
| `openbridge autostart` | Arranque automático (`install`/`remove`) |
|
|
@@ -144,6 +145,32 @@ Ejemplo concreto:
|
|
|
144
145
|
El puente remoto solo necesita salida a internet hacia el hub; no abre puertos ni
|
|
145
146
|
túnel propio. Para que arranque solo en cada PC: `openbridge autostart install`.
|
|
146
147
|
|
|
148
|
+
## Hub en hosting PHP (URL fija, sin túnel)
|
|
149
|
+
|
|
150
|
+
Además del hub Node, OpenBridge puede correr su hub en un **hosting PHP** (cPanel),
|
|
151
|
+
igual que OpenConex, y dejar que cada PC corra solo el puente:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
[Celular] --HTTPS--> openbridge.tamnora.com (hub PHP) --> [PC] openbridge pair
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- Web siempre arriba, **URL fija** y PWA/Web Push estables; sin túneles ni puertos.
|
|
158
|
+
- Cada PC se vincula con un **código**: en la PC `openbridge pair <url>` muestra un
|
|
159
|
+
código y en la web lo ingresás en **Dispositivos -> Agregar PC**. Cada PC queda
|
|
160
|
+
con su **token propio** (revocable); cada usuario ve solo sus PCs (el admin, todas).
|
|
161
|
+
- El frontend es el mismo del hub Node (se copia, no se duplica).
|
|
162
|
+
|
|
163
|
+
Guía completa (requisitos, build, subida y emparejamiento): [`docs/DEPLOY-PHP.md`](docs/DEPLOY-PHP.md).
|
|
164
|
+
|
|
165
|
+
La PC corre el puente; para que siga tras cerrar la consola:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
openbridge bridge --background # arranca en segundo plano
|
|
169
|
+
openbridge bridge --status # ¿corre?
|
|
170
|
+
openbridge bridge --stop # detener
|
|
171
|
+
openbridge autostart install # arranca solo al iniciar sesión
|
|
172
|
+
```
|
|
173
|
+
|
|
147
174
|
## Túnel y URL estable
|
|
148
175
|
|
|
149
176
|
Proveedores (`--tunnel` o `openbridge tunnel <prov>`): `tunnelmole` (gratis, sin
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -682,12 +682,46 @@ async function cmdLogs(argv) {
|
|
|
682
682
|
// ---------------------------------------------------------------------------
|
|
683
683
|
// bridge (solo el puente, para modo hub remoto)
|
|
684
684
|
// ---------------------------------------------------------------------------
|
|
685
|
+
function bridgePid() {
|
|
686
|
+
try {
|
|
687
|
+
return parseInt(fs.readFileSync(paths.bridgeLockPath(), 'utf8'), 10) || 0;
|
|
688
|
+
} catch (e) {
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
685
693
|
async function cmdBridge(argv) {
|
|
686
694
|
const { flags } = parseArgs(argv);
|
|
687
695
|
if (!fs.existsSync(paths.configPath())) {
|
|
688
|
-
console.error('No hay configuracion de puente en ' + paths.home() + '. Corre primero: openbridge join <url> o openbridge
|
|
696
|
+
console.error('No hay configuracion de puente en ' + paths.home() + '. Corre primero: openbridge join <url> o openbridge pair <url>');
|
|
689
697
|
return 1;
|
|
690
698
|
}
|
|
699
|
+
|
|
700
|
+
if (flags.stop) {
|
|
701
|
+
const pid = bridgePid();
|
|
702
|
+
if (!pid || !pidAlive(pid)) {
|
|
703
|
+
try { fs.unlinkSync(paths.bridgeLockPath()); } catch (e) { /* nada */ }
|
|
704
|
+
console.log('El puente no esta corriendo.');
|
|
705
|
+
return 0;
|
|
706
|
+
}
|
|
707
|
+
killPid(pid);
|
|
708
|
+
try { fs.unlinkSync(paths.bridgeLockPath()); } catch (e) { /* nada */ }
|
|
709
|
+
console.log('Puente detenido (pid ' + pid + ').');
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const cfg = config.readBridge();
|
|
714
|
+
if (flags.status) {
|
|
715
|
+
const pid = bridgePid();
|
|
716
|
+
const live = pid && pidAlive(pid);
|
|
717
|
+
console.log('Puente: ' + (live ? 'corriendo (pid ' + pid + ')' : 'detenido'));
|
|
718
|
+
console.log(' hub : ' + (cfg.apiUrl || '(sin configurar)'));
|
|
719
|
+
console.log(' id : ' + (cfg.bridgeId || '(sin id)'));
|
|
720
|
+
console.log(' nombre : ' + (cfg.bridgeName || ''));
|
|
721
|
+
console.log(' log : ' + paths.bridgeLogPath());
|
|
722
|
+
return 0;
|
|
723
|
+
}
|
|
724
|
+
|
|
691
725
|
// Permite apuntar a otro hub sin editar config.json a mano.
|
|
692
726
|
const env = { ...process.env, OPENBRIDGE_HOME: paths.baseDir() };
|
|
693
727
|
if (flags.api) env.OPENBRIDGE_API_URL = String(flags.api);
|
|
@@ -695,6 +729,33 @@ async function cmdBridge(argv) {
|
|
|
695
729
|
if (flags.id) env.OPENBRIDGE_BRIDGE_ID = String(flags.id);
|
|
696
730
|
if (flags.name) env.OPENBRIDGE_BRIDGE_NAME = String(flags.name);
|
|
697
731
|
const bridgePath = path.join(__dirname, 'bridge', 'bridge.js');
|
|
732
|
+
|
|
733
|
+
// Segundo plano: detached + stdio ignorado; sobrevive al cierre de la consola.
|
|
734
|
+
if (flags.background || flags.bg || flags.detach || flags.d) {
|
|
735
|
+
const existing = bridgePid();
|
|
736
|
+
if (existing && pidAlive(existing)) {
|
|
737
|
+
console.log('El puente ya corre en segundo plano (pid ' + existing + ').');
|
|
738
|
+
console.log(' estado: openbridge bridge --status · detener: openbridge bridge --stop');
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
const child = spawn(process.execPath, [bridgePath], {
|
|
742
|
+
cwd: paths.home(),
|
|
743
|
+
env,
|
|
744
|
+
stdio: 'ignore',
|
|
745
|
+
windowsHide: true,
|
|
746
|
+
detached: true,
|
|
747
|
+
});
|
|
748
|
+
child.unref();
|
|
749
|
+
await new Promise((r) => setTimeout(r, 900));
|
|
750
|
+
const pid = bridgePid() || child.pid;
|
|
751
|
+
console.log('Puente en segundo plano (pid ' + pid + ').');
|
|
752
|
+
console.log(' hub : ' + (cfg.apiUrl || '(sin configurar)'));
|
|
753
|
+
console.log(' log : ' + paths.bridgeLogPath());
|
|
754
|
+
console.log(' estado : openbridge bridge --status');
|
|
755
|
+
console.log(' detener: openbridge bridge --stop');
|
|
756
|
+
return 0;
|
|
757
|
+
}
|
|
758
|
+
|
|
698
759
|
const child = spawn(process.execPath, [bridgePath], {
|
|
699
760
|
cwd: paths.home(),
|
|
700
761
|
env,
|
|
@@ -718,11 +779,16 @@ function hubApiUrl(raw) {
|
|
|
718
779
|
return u;
|
|
719
780
|
}
|
|
720
781
|
|
|
782
|
+
// Reenvia `--background` a `openbridge bridge` cuando join/pair lo piden.
|
|
783
|
+
function bridgeStartArgs(flags) {
|
|
784
|
+
return (flags.background || flags.bg || flags.detach || flags.d) ? ['--background'] : [];
|
|
785
|
+
}
|
|
786
|
+
|
|
721
787
|
async function cmdJoin(argv) {
|
|
722
788
|
const { flags, _ } = parseArgs(argv);
|
|
723
789
|
const url = String(_[0] || flags.api || '').trim();
|
|
724
790
|
if (!url || !/^https?:\/\//i.test(url)) {
|
|
725
|
-
console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start]');
|
|
791
|
+
console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start] [--background]');
|
|
726
792
|
console.error('Ej.: openbridge join https://mi-pc.trycloudflare.com --token <t> --id pc2 --name "PC oficina"');
|
|
727
793
|
console.error('(acepta la base, /chat.php o /api.php; se normaliza a /api.php)');
|
|
728
794
|
return 1;
|
|
@@ -747,7 +813,102 @@ async function cmdJoin(argv) {
|
|
|
747
813
|
}
|
|
748
814
|
console.log('');
|
|
749
815
|
console.log('Arrancando el puente (Ctrl+C para salir)...');
|
|
750
|
-
return cmdBridge(
|
|
816
|
+
return cmdBridge(bridgeStartArgs(flags));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// pair: empareja esta PC con un hub PHP por codigo (device code). La PC pide
|
|
821
|
+
// un codigo, el usuario lo tipea en la web (Dispositivos -> Agregar PC) y la PC
|
|
822
|
+
// recibe su token propio. Necesita un hub PHP (openbridge.tamnora.com).
|
|
823
|
+
// ---------------------------------------------------------------------------
|
|
824
|
+
async function pairApi(apiUrl, action, body) {
|
|
825
|
+
const res = await fetch(apiUrl + '?action=' + action, {
|
|
826
|
+
method: 'POST',
|
|
827
|
+
headers: { 'Content-Type': 'application/json' },
|
|
828
|
+
body: JSON.stringify(body || {}),
|
|
829
|
+
});
|
|
830
|
+
let json = null;
|
|
831
|
+
try { json = await res.json(); } catch (e) { json = null; }
|
|
832
|
+
if (!json) throw new Error('respuesta invalida del hub (HTTP ' + res.status + ')');
|
|
833
|
+
return json;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function cmdPair(argv) {
|
|
837
|
+
const { flags, _ } = parseArgs(argv);
|
|
838
|
+
const url = String(_[0] || flags.hub || flags.api || '').trim();
|
|
839
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
840
|
+
console.error('Uso: openbridge pair <url-del-hub> [--id <pc>] [--name "<nombre>"] [--no-start] [--background]');
|
|
841
|
+
console.error('Ej.: openbridge pair https://openbridge.tamnora.com --name "PC 1"');
|
|
842
|
+
return 1;
|
|
843
|
+
}
|
|
844
|
+
paths.ensureDirs();
|
|
845
|
+
const cfg = config.readBridge();
|
|
846
|
+
const apiUrl = hubApiUrl(url);
|
|
847
|
+
const bridgeId = sanitizeId(flags.id || cfg.bridgeId || os.hostname());
|
|
848
|
+
const bridgeName = (typeof flags.name === 'string' && flags.name.trim())
|
|
849
|
+
? flags.name.trim().slice(0, 40)
|
|
850
|
+
: (cfg.bridgeName || bridgeId);
|
|
851
|
+
|
|
852
|
+
let start;
|
|
853
|
+
try {
|
|
854
|
+
start = await pairApi(apiUrl, 'bridge_pair_start', { bridge_id: bridgeId, bridge_name: bridgeName });
|
|
855
|
+
} catch (e) {
|
|
856
|
+
console.error('No pude iniciar el emparejamiento: ' + e.message);
|
|
857
|
+
return 1;
|
|
858
|
+
}
|
|
859
|
+
if (!start.ok) {
|
|
860
|
+
console.error('El hub rechazo el emparejamiento: ' + (start.error || 'error'));
|
|
861
|
+
return 1;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const verify = start.verify_url || url;
|
|
865
|
+
console.log('Para vincular esta PC a tu cuenta:');
|
|
866
|
+
console.log(' 1. Abri ' + verify + ' y logueate.');
|
|
867
|
+
console.log(' 2. Anda a Dispositivos -> Agregar PC.');
|
|
868
|
+
console.log(' 3. Ingresa el codigo: ' + start.user_code);
|
|
869
|
+
console.log('');
|
|
870
|
+
console.log('Esperando aprobacion (vence en ' + Math.round((parseInt(start.expires_in, 10) || 600) / 60) + ' min)...');
|
|
871
|
+
|
|
872
|
+
const deadline = Date.now() + (parseInt(start.expires_in, 10) || 600) * 1000;
|
|
873
|
+
let token = '';
|
|
874
|
+
let finalId = bridgeId;
|
|
875
|
+
while (Date.now() < deadline) {
|
|
876
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
877
|
+
let p;
|
|
878
|
+
try {
|
|
879
|
+
p = await pairApi(apiUrl, 'bridge_pair_poll', { device_code: start.device_code });
|
|
880
|
+
} catch (e) {
|
|
881
|
+
process.stdout.write('.');
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (p.status === 'approved' && p.bridge_token) {
|
|
885
|
+
token = p.bridge_token;
|
|
886
|
+
finalId = p.bridge_id || bridgeId;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
if (p.status === 'expired' || p.status === 'unknown') {
|
|
890
|
+
console.error('\nEl codigo vencio. Volve a correr: openbridge pair ' + url);
|
|
891
|
+
return 1;
|
|
892
|
+
}
|
|
893
|
+
process.stdout.write('.');
|
|
894
|
+
}
|
|
895
|
+
if (!token) {
|
|
896
|
+
console.error('\nSe agoto el tiempo. Volve a correr: openbridge pair ' + url);
|
|
897
|
+
return 1;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
cfg.apiUrl = apiUrl;
|
|
901
|
+
cfg.apiToken = token;
|
|
902
|
+
cfg.bridgeId = finalId;
|
|
903
|
+
cfg.bridgeName = bridgeName;
|
|
904
|
+
config.writeBridge(cfg);
|
|
905
|
+
console.log('\nPC emparejada: ' + finalId + ' (' + bridgeName + ')');
|
|
906
|
+
if (flags['no-start']) {
|
|
907
|
+
console.log('Arrancala con: openbridge bridge');
|
|
908
|
+
return 0;
|
|
909
|
+
}
|
|
910
|
+
console.log('Arrancando el puente (Ctrl+C para salir)...');
|
|
911
|
+
return cmdBridge(bridgeStartArgs(flags));
|
|
751
912
|
}
|
|
752
913
|
|
|
753
914
|
// ---------------------------------------------------------------------------
|
|
@@ -967,12 +1128,18 @@ async function cmdAutostart(argv) {
|
|
|
967
1128
|
}
|
|
968
1129
|
try { fs.mkdirSync(path.dirname(target), { recursive: true }); } catch (e) { /* nada */ }
|
|
969
1130
|
const home = paths.baseDir();
|
|
1131
|
+
// Si esta PC es un puente de un hub remoto (join/pair), el autostart corre
|
|
1132
|
+
// solo el puente; si es el hub local, corre el server.
|
|
1133
|
+
let acfg = { apiUrl: '' };
|
|
1134
|
+
try { acfg = config.readBridge(); } catch (e) { /* nada */ }
|
|
1135
|
+
const remote = /^https?:\/\//i.test(acfg.apiUrl || '') && !/127\.0\.0\.1|localhost/i.test(acfg.apiUrl);
|
|
1136
|
+
const runArgs = remote ? 'bridge' : 'server --stream --no-tunnel';
|
|
970
1137
|
if (process.platform === 'win32') {
|
|
971
1138
|
const ps = [
|
|
972
1139
|
'$ws = New-Object -ComObject WScript.Shell;',
|
|
973
1140
|
'$sc = $ws.CreateShortcut(' + JSON.stringify(target) + ');',
|
|
974
1141
|
'$sc.TargetPath = ' + JSON.stringify(node) + ';',
|
|
975
|
-
'$sc.Arguments = ' + JSON.stringify('"' + bin + '"
|
|
1142
|
+
'$sc.Arguments = ' + JSON.stringify('"' + bin + '" ' + runArgs) + ';',
|
|
976
1143
|
'$sc.WorkingDirectory = ' + JSON.stringify(home) + ';',
|
|
977
1144
|
'$sc.WindowStyle = 7;',
|
|
978
1145
|
'$sc.Save();',
|
|
@@ -985,7 +1152,8 @@ async function cmdAutostart(argv) {
|
|
|
985
1152
|
+ '<plist version="1.0"><dict>'
|
|
986
1153
|
+ '<key>Label</key><string>net.openbridge.server</string>'
|
|
987
1154
|
+ '<key>ProgramArguments</key><array>'
|
|
988
|
-
+ '<string>' + node + '</string><string>' + bin + '</string
|
|
1155
|
+
+ '<string>' + node + '</string><string>' + bin + '</string>'
|
|
1156
|
+
+ runArgs.split(' ').map((a) => '<string>' + a + '</string>').join('')
|
|
989
1157
|
+ '</array>'
|
|
990
1158
|
+ '<key>WorkingDirectory</key><string>' + home + '</string>'
|
|
991
1159
|
+ '<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>'
|
|
@@ -994,7 +1162,7 @@ async function cmdAutostart(argv) {
|
|
|
994
1162
|
} else {
|
|
995
1163
|
const unit = '[Unit]\nDescription=OpenBridge\nAfter=network-online.target\n\n'
|
|
996
1164
|
+ '[Service]\nType=simple\n'
|
|
997
|
-
+ 'ExecStart=' + node + ' ' + bin + '
|
|
1165
|
+
+ 'ExecStart=' + node + ' ' + bin + ' ' + runArgs + '\n'
|
|
998
1166
|
+ 'WorkingDirectory=' + home + '\nRestart=on-failure\n\n'
|
|
999
1167
|
+ '[Install]\nWantedBy=default.target\n';
|
|
1000
1168
|
fs.writeFileSync(target, unit);
|
|
@@ -1002,7 +1170,7 @@ async function cmdAutostart(argv) {
|
|
|
1002
1170
|
spawnSync('systemctl', ['--user', 'enable', '--now', 'openbridge.service'], { windowsHide: true });
|
|
1003
1171
|
}
|
|
1004
1172
|
console.log('autostart instalado: ' + target);
|
|
1005
|
-
console.log(' (arranca `openbridge
|
|
1173
|
+
console.log(' (arranca `openbridge ' + runArgs + '` en ' + home + ')');
|
|
1006
1174
|
return 0;
|
|
1007
1175
|
}
|
|
1008
1176
|
|
|
@@ -1107,8 +1275,9 @@ function usage() {
|
|
|
1107
1275
|
console.log(' qr Muestra la URL (publica o local) como QR para el celular');
|
|
1108
1276
|
console.log(' tunnel Muestra o cambia el proveedor de tunel (tunnelmole|ngrok|cloudflare|none) [--domain]');
|
|
1109
1277
|
console.log(' logs Ultimas lineas de los logs (--follow --server --bridge)');
|
|
1110
|
-
console.log(' bridge Corre solo el puente (--
|
|
1278
|
+
console.log(' bridge Corre solo el puente (--background | --stop | --status)');
|
|
1111
1279
|
console.log(' join Vincula esta PC como puente de un hub (<url> --token --id --name)');
|
|
1280
|
+
console.log(' pair Empareja esta PC con un hub PHP por codigo (<url> [--id --name])');
|
|
1112
1281
|
console.log(' import Trae data/ de OpenConex (<data-dir> [--force])');
|
|
1113
1282
|
console.log(' reset Borra todos los chats/datos (--session <id> --yes)');
|
|
1114
1283
|
console.log(' autostart Instala/quita el arranque automatico (install|remove)');
|
|
@@ -1144,6 +1313,7 @@ async function main(argv) {
|
|
|
1144
1313
|
case 'logs': return cmdLogs(args.slice(1));
|
|
1145
1314
|
case 'bridge': return cmdBridge(args.slice(1));
|
|
1146
1315
|
case 'join': return cmdJoin(args.slice(1));
|
|
1316
|
+
case 'pair': return cmdPair(args.slice(1));
|
|
1147
1317
|
case 'import': return cmdImport(args.slice(1));
|
|
1148
1318
|
case 'reset': return cmdReset(args.slice(1));
|
|
1149
1319
|
case 'autostart': return cmdAutostart(args.slice(1));
|
package/src/web/assets/app.js
CHANGED
|
@@ -67,6 +67,7 @@ var els = {
|
|
|
67
67
|
viewProcs: document.getElementById('viewProcs'),
|
|
68
68
|
viewChanges: document.getElementById('viewChanges'),
|
|
69
69
|
viewMcp: document.getElementById('viewMcp'),
|
|
70
|
+
viewDevices: document.getElementById('viewDevices'),
|
|
70
71
|
themeColor: document.getElementById('themeColor'),
|
|
71
72
|
themeLink: document.getElementById('themeStylesheet'),
|
|
72
73
|
statusbar: document.getElementById('statusbar'),
|
|
@@ -146,6 +147,7 @@ var state = {
|
|
|
146
147
|
catVer: '',
|
|
147
148
|
bridges: [], // resumen de puentes registrados [{id,name,online,busy_session}]
|
|
148
149
|
activeBridge: '', // puente seleccionado en el sidebar (id)
|
|
150
|
+
features: {}, // capacidades del hub (p. ej. {pairing:true} en el hub PHP)
|
|
149
151
|
isSending: false,
|
|
150
152
|
pendingImage: null,
|
|
151
153
|
messages: [],
|
|
@@ -431,6 +433,11 @@ async function refreshBridgeData() {
|
|
|
431
433
|
// Aplica la respuesta de bootstrap (o de un refresh tras cambio de puente).
|
|
432
434
|
function applyBootPayload(data) {
|
|
433
435
|
if (!data || !data.ok) return;
|
|
436
|
+
if (data.features) {
|
|
437
|
+
state.features = data.features;
|
|
438
|
+
var devBtn = document.querySelector('#sidebar .utilities button[data-view="devices"]');
|
|
439
|
+
if (devBtn) devBtn.style.display = state.features.pairing ? '' : 'none';
|
|
440
|
+
}
|
|
434
441
|
var newB = Array.isArray(data.bridges) ? data.bridges : null;
|
|
435
442
|
if (newB && newB.length) {
|
|
436
443
|
// El puente activo guardado puede no existir (o ser el legacy '' tras
|
|
@@ -1011,6 +1018,7 @@ function showView(name) {
|
|
|
1011
1018
|
els.viewProcs.style.display = 'none';
|
|
1012
1019
|
els.viewChanges.style.display = 'none';
|
|
1013
1020
|
els.viewMcp.style.display = 'none';
|
|
1021
|
+
if (els.viewDevices) els.viewDevices.style.display = 'none';
|
|
1014
1022
|
|
|
1015
1023
|
if (name !== 'procs') stopProcView();
|
|
1016
1024
|
|
|
@@ -1089,6 +1097,12 @@ function showView(name) {
|
|
|
1089
1097
|
els.sendForm.style.display = 'none';
|
|
1090
1098
|
els.btnCmds.style.display = 'none';
|
|
1091
1099
|
renderMcpView();
|
|
1100
|
+
} else if (name === 'devices') {
|
|
1101
|
+
els.viewDevices.style.display = '';
|
|
1102
|
+
els.btnBack.style.display = 'none';
|
|
1103
|
+
els.sendForm.style.display = 'none';
|
|
1104
|
+
els.btnCmds.style.display = 'none';
|
|
1105
|
+
renderDevicesView();
|
|
1092
1106
|
}
|
|
1093
1107
|
|
|
1094
1108
|
if (els.fabNew) els.fabNew.classList.toggle('show', name === 'home');
|
|
@@ -3062,6 +3076,79 @@ async function loadMcp() {
|
|
|
3062
3076
|
+ esc(output || 'No hay servidores MCP configurados.') + '</pre>';
|
|
3063
3077
|
}
|
|
3064
3078
|
|
|
3079
|
+
// ---------------------------------------------------------------------------
|
|
3080
|
+
// Vista Dispositivos: PCs emparejadas a la cuenta (solo hub PHP). El usuario
|
|
3081
|
+
// corre `openbridge pair <url>` en su PC, ve un codigo y lo ingresa aca.
|
|
3082
|
+
// ---------------------------------------------------------------------------
|
|
3083
|
+
function renderDevicesView() {
|
|
3084
|
+
els.hTitle.textContent = 'dispositivos';
|
|
3085
|
+
els.hSub.textContent = 'tus PCs emparejadas';
|
|
3086
|
+
loadDevices();
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
async function loadDevices() {
|
|
3090
|
+
if (!els.viewDevices) return;
|
|
3091
|
+
var head = '<div class="view-head"><h2>mis PCs</h2>'
|
|
3092
|
+
+ '<div class="view-sub">emparejadas a tu cuenta</div></div>';
|
|
3093
|
+
var res = await api('api.php?action=bridges');
|
|
3094
|
+
if (!res.ok) {
|
|
3095
|
+
els.viewDevices.innerHTML = head + '<div class="placeholder">' + esc(res.error || 'no se pudo consultar') + '</div>';
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
els.viewDevices.innerHTML = head
|
|
3099
|
+
+ '<div class="dev-add">'
|
|
3100
|
+
+ '<input type="text" id="devCode" placeholder="codigo (ej: ABCD-EFGH)" autocomplete="off" spellcheck="false">'
|
|
3101
|
+
+ '<button type="button" class="linkbtn" id="devAdd">agregar PC</button>'
|
|
3102
|
+
+ '</div>'
|
|
3103
|
+
+ '<div class="view-sub">En tu PC: <code>openbridge pair ' + esc(location.origin) + '</code> y te muestra un codigo. Ingresalo aca.</div>'
|
|
3104
|
+
+ '<div id="devList"></div>';
|
|
3105
|
+
var input = document.getElementById('devCode');
|
|
3106
|
+
var btn = document.getElementById('devAdd');
|
|
3107
|
+
if (btn) btn.addEventListener('click', function () { addDevice(input.value); });
|
|
3108
|
+
if (input) input.addEventListener('keydown', function (e) { if (e.key === 'Enter') addDevice(input.value); });
|
|
3109
|
+
renderDeviceList(res.bridges || []);
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
function renderDeviceList(list) {
|
|
3113
|
+
var box = document.getElementById('devList');
|
|
3114
|
+
if (!box) return;
|
|
3115
|
+
if (!list.length) {
|
|
3116
|
+
box.innerHTML = '<div class="placeholder">Todavia no emparejaste ninguna PC.</div>';
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
var html = '';
|
|
3120
|
+
for (var i = 0; i < list.length; i++) {
|
|
3121
|
+
var b = list[i];
|
|
3122
|
+
html += '<div class="device"><span class="dot"' + (b.online ? '' : ' style="background:var(--muted)"') + '></span>'
|
|
3123
|
+
+ '<b>' + esc(b.name) + '</b><span class="dnote">' + esc(b.id) + '</span>'
|
|
3124
|
+
+ '<button type="button" class="linkbtn" data-revoke="' + esc(b.id) + '">desvincular</button></div>';
|
|
3125
|
+
}
|
|
3126
|
+
box.innerHTML = html;
|
|
3127
|
+
var btns = box.querySelectorAll('[data-revoke]');
|
|
3128
|
+
for (var j = 0; j < btns.length; j++) {
|
|
3129
|
+
btns[j].addEventListener('click', function () { revokeDevice(this.getAttribute('data-revoke')); });
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
async function addDevice(code) {
|
|
3134
|
+
code = (code || '').trim();
|
|
3135
|
+
if (!code) return;
|
|
3136
|
+
var res = await fetch('api.php?action=bridge_pair_approve', apiCsrf('POST', { user_code: code }))
|
|
3137
|
+
.then(function (r) { return r.json(); })
|
|
3138
|
+
.catch(function () { return { ok: false, error: 'network' }; });
|
|
3139
|
+
if (!res.ok) { alert(res.error || 'no se pudo emparejar'); return; }
|
|
3140
|
+
loadDevices();
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
async function revokeDevice(id) {
|
|
3144
|
+
if (!window.confirm('Desvincular la PC "' + id + '"?')) return;
|
|
3145
|
+
var res = await fetch('api.php?action=bridge_revoke', apiCsrf('POST', { id: id }))
|
|
3146
|
+
.then(function (r) { return r.json(); })
|
|
3147
|
+
.catch(function () { return { ok: false, error: 'network' }; });
|
|
3148
|
+
if (!res.ok) { alert(res.error || 'no se pudo desvincular'); return; }
|
|
3149
|
+
loadDevices();
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3065
3152
|
function chgStatusClass(st) {
|
|
3066
3153
|
if (st === '??' || st === 'A') return 'add';
|
|
3067
3154
|
if (st.indexOf('D') >= 0) return 'del';
|
|
@@ -378,6 +378,14 @@
|
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
/* ----- Tab de procesos (dev servers de la sesión activa) ----- */
|
|
381
|
+
#viewDevices .dev-add { display: flex; gap: 8px; margin: 10px 0; }
|
|
382
|
+
#viewDevices .dev-add input { flex: 1; min-width: 0; padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); color: var(--text); font: inherit; }
|
|
383
|
+
#viewDevices .dev-add input:focus { border-color: var(--accent); outline: none; }
|
|
384
|
+
#viewDevices .device { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 8px; background: var(--panel); }
|
|
385
|
+
#viewDevices .device .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok, #22c55e); flex-shrink: 0; }
|
|
386
|
+
#viewDevices .device .dnote { color: var(--muted); font-size: 11.5px; }
|
|
387
|
+
#viewDevices .device .linkbtn { margin-left: auto; }
|
|
388
|
+
#viewDevices code { background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 1px 6px; font-size: 11.5px; }
|
|
381
389
|
#viewProcs .pp-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
382
390
|
#viewProcs .pp-head b { font-size: 16px; font-weight: 600; }
|
|
383
391
|
#viewProcs .pp-head b::before { content: "❯ "; color: var(--mine); }
|
|
@@ -1204,6 +1212,7 @@
|
|
|
1204
1212
|
<button data-view="procs" type="button" title="Procesos de la sesión">procs</button>
|
|
1205
1213
|
<button data-view="changes" type="button" title="Cambios del proyecto (git)">cambios</button>
|
|
1206
1214
|
<button data-view="mcp" type="button" title="Servidores MCP de opencode">mcp</button>
|
|
1215
|
+
<button data-view="devices" type="button" title="PCs emparejadas a tu cuenta" style="display:none">dispositivos</button>
|
|
1207
1216
|
</div>
|
|
1208
1217
|
<div class="footer">
|
|
1209
1218
|
<div class="row" id="bridgeStatusRow">
|
|
@@ -1287,6 +1296,7 @@
|
|
|
1287
1296
|
<div id="viewProcs" class="view" style="display:none"></div>
|
|
1288
1297
|
<div id="viewChanges" class="view" style="display:none"></div>
|
|
1289
1298
|
<div id="viewMcp" class="view" style="display:none"></div>
|
|
1299
|
+
<div id="viewDevices" class="view" style="display:none"></div>
|
|
1290
1300
|
|
|
1291
1301
|
<footer id="statusbar">
|
|
1292
1302
|
<span class="dot" id="sbDot"></span>
|