nio-cat 0.3.0 → 0.3.2
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/bin/nio.js +20 -10
- package/package.json +1 -1
- package/src/pair.js +55 -18
- package/src/server.js +3 -2
- package/web/app/app.js +30 -10
- package/web/app/style.css +2 -0
package/bin/nio.js
CHANGED
|
@@ -11,7 +11,7 @@ const { Hub } = require('../src/hub');
|
|
|
11
11
|
const { createServer } = require('../src/server');
|
|
12
12
|
const { detectAll } = require('../src/agents');
|
|
13
13
|
const { startTunnel } = require('../src/tunnel');
|
|
14
|
-
const { pairLink, terminalQr, writePairPage } = require('../src/pair');
|
|
14
|
+
const { pairLink, terminalQr, writePairPage, makeCode, codeLink, terminalQrSize } = require('../src/pair');
|
|
15
15
|
const { which } = require('../src/proc');
|
|
16
16
|
|
|
17
17
|
const argv = process.argv.slice(2);
|
|
@@ -81,13 +81,24 @@ function openDashboard() {
|
|
|
81
81
|
else console.log(` ${c.dim('Panell:')} ${link}`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function showPair(publicUrl) {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
async function showPair(publicUrl) {
|
|
85
|
+
const app = flags.app || cfg.app;
|
|
86
|
+
const link = pairLink(app, publicUrl, cfg.token), code = makeCode(publicUrl, cfg.token), qrLink = codeLink(app, code);
|
|
87
|
+
const file = writePairPage(link, code, qrLink);
|
|
88
|
+
// El QR ha de cabre al terminal: marge normal, marge d'1 mòdul i, si no hi cap, la pàgina del navegador.
|
|
89
|
+
const rows = process.stdout.rows || 999, cols = process.stdout.columns || 999;
|
|
90
|
+
const fits = (m) => { const z = terminalQrSize(qrLink, m); return z.rows + 9 <= rows && z.cols + 2 <= cols; };
|
|
91
|
+
const margin = fits(2) ? 2 : fits(1) ? 1 : 0;
|
|
92
|
+
if (margin) {
|
|
93
|
+
console.log(`\n ${c.b('Escaneja aquest codi amb el mòbil')}\n`);
|
|
94
|
+
console.log(terminalQr(qrLink, { margin }));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(`\n ${c.warn('El terminal és massa petit per al QR.')} Fes-lo més gran o obre ${file}`);
|
|
97
|
+
if (canOpenBrowser()) openUrl(file);
|
|
98
|
+
}
|
|
99
|
+
console.log(`\n ${c.b('Codi:')} ${code}`);
|
|
100
|
+
console.log(c.dim(' Copia\'l i enganxa\'l a Sessions → Tria un ordinador.'));
|
|
101
|
+
console.log(c.dim(' Aquest codi dona accés als teus agents. Guarda\'l com una contrasenya.\n'));
|
|
91
102
|
if (flags.open) {
|
|
92
103
|
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
93
104
|
try {
|
|
@@ -98,7 +109,6 @@ function showPair(publicUrl) {
|
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
|
|
102
112
|
// nio ssh usuari@servidor: engega nio al servidor (si no hi és), obre un túnel SSH i obre el panell ja connectat.
|
|
103
113
|
const shq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
|
|
104
114
|
function remoteScript(port, dir) {
|
|
@@ -260,7 +270,7 @@ async function start() {
|
|
|
260
270
|
console.log(' Encara no hi ha cap adreça desada. Engega nio primer.');
|
|
261
271
|
break;
|
|
262
272
|
}
|
|
263
|
-
showPair(flags.url || cfg.lastUrl);
|
|
273
|
+
await showPair(flags.url || cfg.lastUrl);
|
|
264
274
|
break;
|
|
265
275
|
case 'reset-token':
|
|
266
276
|
cfg.token = crypto.randomBytes(24).toString('base64url');
|
package/package.json
CHANGED
package/src/pair.js
CHANGED
|
@@ -12,6 +12,22 @@ function pairLink(app, url, token) {
|
|
|
12
12
|
return `${app.replace(/\/$/, '')}/app#u=${u}&t=${encodeURIComponent(token)}`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Codi de connexió: l'adreça i la clau dins d'una sola cadena («nio-…»). No cal cap servidor.
|
|
16
|
+
// La web ja sap posar-hi l'enllaç de davant. Les adreces de Cloudflare es comprimeixen (≈ 60 caràcters).
|
|
17
|
+
const CF = '.trycloudflare.com';
|
|
18
|
+
function makeCode(url, token) {
|
|
19
|
+
const raw = Buffer.from(token, 'base64url'), tok24 = raw.length === 24 && raw.toString('base64url') === token;
|
|
20
|
+
const sub = url.startsWith('https://') && url.endsWith(CF) ? url.slice(8, -CF.length) : null;
|
|
21
|
+
const lan = url.match(/^http:\/\/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}):(\d{1,5})$/);
|
|
22
|
+
let bytes;
|
|
23
|
+
if (tok24 && sub && /^[\w-]+$/.test(sub)) bytes = Buffer.concat([Buffer.from([1]), raw, Buffer.from(sub)]);
|
|
24
|
+
else if (tok24 && lan && lan.slice(1, 5).every((n) => +n < 256) && +lan[5] < 65536) bytes = Buffer.concat([Buffer.from([2]), raw, Buffer.from(lan.slice(1, 5).map(Number)), Buffer.from([+lan[5] >> 8, +lan[5] & 255])]);
|
|
25
|
+
else bytes = Buffer.concat([Buffer.from([0]), Buffer.from(`${url}\n${token}`)]);
|
|
26
|
+
return `nio-${bytes.toString('base64url')}`;
|
|
27
|
+
}
|
|
28
|
+
// Enllaç curt per al QR: una adreça de l'app amb el codi al fragment. Un QR més petit cap millor al terminal.
|
|
29
|
+
const codeLink = (app, code) => `${app.replace(/\/$/, '')}/app#${code}`;
|
|
30
|
+
|
|
15
31
|
function qr(text) {
|
|
16
32
|
const q = qrcode(0, 'L');
|
|
17
33
|
q.addData(text);
|
|
@@ -19,34 +35,49 @@ function qr(text) {
|
|
|
19
35
|
return q;
|
|
20
36
|
}
|
|
21
37
|
|
|
22
|
-
// QR per al terminal:
|
|
23
|
-
//
|
|
24
|
-
function terminalQr(text, { color = !process.env.NO_COLOR } = {}) {
|
|
38
|
+
// QR per al terminal: cada mòdul són dos espais amb color de fons (negre o blanc). No depèn de cap glif,
|
|
39
|
+
// així que no es talla amb tipus de lletra o interlineats estranys. Sempre negre sobre blanc, amb tema clar o fosc.
|
|
40
|
+
function terminalQr(text, { color = !process.env.NO_COLOR, margin = 2 } = {}) {
|
|
25
41
|
const q = qr(text);
|
|
26
42
|
const n = q.getModuleCount();
|
|
27
|
-
const M =
|
|
43
|
+
const M = margin; // marge blanc al voltant, en mòduls
|
|
28
44
|
const dark = (r, c) => r >= 0 && c >= 0 && r < n && c < n && q.isDark(r, c);
|
|
29
45
|
const lines = [];
|
|
30
|
-
for (let r = -M; r < n + M; r
|
|
31
|
-
let line = '';
|
|
46
|
+
for (let r = -M; r < n + M; r++) {
|
|
47
|
+
let line = '', prev = null;
|
|
32
48
|
for (let c = -M; c < n + M; c++) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
const d = dark(r, c);
|
|
50
|
+
if (color) {
|
|
51
|
+
if (d !== prev) line += d ? '\x1b[40m' : '\x1b[107m';
|
|
52
|
+
line += ' ';
|
|
53
|
+
} else line += d ? ' ' : '██';
|
|
54
|
+
prev = d;
|
|
37
55
|
}
|
|
38
|
-
lines.push(
|
|
56
|
+
lines.push(` ${line}${color ? '\x1b[0m' : ''}`);
|
|
39
57
|
}
|
|
40
58
|
return lines.join('\n');
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
// SVG propi: una sola forma amb tires horitzontals de mòduls, vora de 4 mòduls i vores nítides.
|
|
62
|
+
// (El de la llibreria dibuixa cada mòdul per separat i, en escalar-lo, deixa línies fines entre mòduls.)
|
|
43
63
|
function qrSvg(text) {
|
|
44
|
-
|
|
64
|
+
const q = qr(text), n = q.getModuleCount(), M = 4;
|
|
65
|
+
let d = '';
|
|
66
|
+
for (let r = 0; r < n; r++) {
|
|
67
|
+
for (let c = 0; c < n; c++) {
|
|
68
|
+
if (!q.isDark(r, c)) continue;
|
|
69
|
+
let w = 1;
|
|
70
|
+
while (c + w < n && q.isDark(r, c + w)) w++;
|
|
71
|
+
d += `M${c + M} ${r + M}h${w}v1h-${w}z`;
|
|
72
|
+
c += w - 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const size = n + M * 2;
|
|
76
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges" role="img" aria-label="Codi QR"><rect width="${size}" height="${size}" fill="#fff"/><path d="${d}" fill="#000"/></svg>`;
|
|
45
77
|
}
|
|
46
78
|
|
|
47
|
-
function writePairPage(link) {
|
|
48
|
-
const
|
|
49
|
-
const svg = q.createSvgTag({ cellSize: 6, margin: 4, scalable: true });
|
|
79
|
+
function writePairPage(link, code, qrLink) {
|
|
80
|
+
const svg = qrSvg(qrLink || link);
|
|
50
81
|
const html = `<!doctype html><html lang="ca"><head><meta charset="utf-8">
|
|
51
82
|
<meta name="viewport" content="width=device-width,initial-scale=1"><title>nio // connecta el mòbil</title>
|
|
52
83
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
|
|
@@ -55,11 +86,17 @@ main{width:min(420px,100% - 40px);padding:40px 0}h1{font-size:30px;font-weight:5
|
|
|
55
86
|
.qr{background:#fffefd;border:1px solid #cec7bd;padding:18px;border-radius:16px;margin:24px 0}.qr svg{width:100%;height:auto;display:block}
|
|
56
87
|
code{display:block;word-break:break-all;font:12px/1.5 ui-monospace,Menlo,monospace;background:#e6e1d8;padding:12px 14px;border-radius:12px;color:#272224}small{display:block;margin-top:20px;font-size:13px;color:#69615c}</style></head>
|
|
57
88
|
<body><main><h1>Connecta el mòbil</h1><p>Escaneja el codi amb la càmera. S’obrirà nio ja connectat a aquest ordinador.</p><div class="qr">${svg}</div>
|
|
58
|
-
<p>
|
|
59
|
-
<small>Aquest enllaç dona accés als teus agents. Guarda’l com una contrasenya.</small></main></body></html>`;
|
|
89
|
+
<p>O copia aquest codi i enganxa'l a <b>Sessions → Tria un ordinador</b>:</p><code>${code}</code><small>Aquest codi dona accés als teus agents. Guarda'l com una contrasenya.</small></main></body></html>`;
|
|
60
90
|
const file = path.join(HOME, 'pair.html');
|
|
61
91
|
fs.writeFileSync(file, html, { mode: 0o600 });
|
|
62
92
|
return file;
|
|
63
93
|
}
|
|
64
94
|
|
|
65
|
-
module.exports = { pairLink, terminalQr, writePairPage, qrSvg };
|
|
95
|
+
module.exports = { pairLink, terminalQr, writePairPage, qrSvg, makeCode, codeLink };
|
|
96
|
+
|
|
97
|
+
// Mida del QR al terminal: files i columnes que ocupa amb un marge donat.
|
|
98
|
+
function terminalQrSize(text, margin = 2) {
|
|
99
|
+
const n = qr(text).getModuleCount() + margin * 2;
|
|
100
|
+
return { rows: n, cols: n * 2 + 2 };
|
|
101
|
+
}
|
|
102
|
+
module.exports.terminalQrSize = terminalQrSize;
|
package/src/server.js
CHANGED
|
@@ -10,7 +10,7 @@ const ollama = require('./agents/ollama');
|
|
|
10
10
|
const { save } = require('./config');
|
|
11
11
|
const { httpErr } = require('./hub');
|
|
12
12
|
const github = require('./github');
|
|
13
|
-
const { pairLink, qrSvg } = require('./pair');
|
|
13
|
+
const { pairLink, qrSvg, makeCode, codeLink } = require('./pair');
|
|
14
14
|
|
|
15
15
|
// L'app web: copiada dins del paquet en publicar-lo, o l'arrel del repositori en desenvolupament.
|
|
16
16
|
const WEB_DIRS = [path.join(__dirname, '..', 'web'), path.join(__dirname, '..', '..')];
|
|
@@ -232,7 +232,8 @@ function createServer({ cfg, hub, getPublicUrl, getApp }) {
|
|
|
232
232
|
const pub = getPublicUrl();
|
|
233
233
|
if (!pub || /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/i.test(pub)) return { link: null };
|
|
234
234
|
const link = pairLink(getApp ? getApp() : cfg.app, pub, cfg.token);
|
|
235
|
-
|
|
235
|
+
const code = makeCode(pub, cfg.token);
|
|
236
|
+
return { link, code, svg: qrSvg(codeLink(getApp ? getApp() : cfg.app, code)), url: pub, lan: /^http:\/\//.test(pub) };
|
|
236
237
|
}
|
|
237
238
|
if (p === '/api/github' && method === 'GET') return github.status();
|
|
238
239
|
if (p === '/api/github/repos' && method === 'GET') return github.repos(url.searchParams.get('owner') || '');
|
package/web/app/app.js
CHANGED
|
@@ -191,6 +191,21 @@ function parsePair(text) {
|
|
|
191
191
|
if (!url || !token) return null;
|
|
192
192
|
return { url: url.replace(/\/$/, ''), token };
|
|
193
193
|
}
|
|
194
|
+
// Codi «nio-…»: porta l'adreça i la clau dins (vegeu makeCode al programa). Es descodifica aquí, sense servidor.
|
|
195
|
+
const CF = '.trycloudflare.com';
|
|
196
|
+
function parseCode(text) {
|
|
197
|
+
const m = String(text || '').replace(/\s+/g, '').match(/(?:^|#)nio-([\w-]+)$/i); if (!m) return null;
|
|
198
|
+
try {
|
|
199
|
+
const bin = atob(m[1].replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (m[1].length % 4)) % 4)), u = Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
200
|
+
const tok = () => btoa(String.fromCharCode(...u.slice(1, 25))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
201
|
+
if (u[0] === 1) return { url: `https://${new TextDecoder().decode(u.slice(25))}${CF}`, token: tok() };
|
|
202
|
+
if (u[0] === 2) return { url: `http://${[...u.slice(25, 29)].join('.')}:${(u[29] << 8) | u[30]}`, token: tok() };
|
|
203
|
+
if (u[0] === 0) { const [url, token] = new TextDecoder().decode(u.slice(1)).split('\n'); if (url && token) return { url: url.replace(/\/$/, ''), token }; }
|
|
204
|
+
} catch { /* codi malmès */ }
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
// Un codi o un enllaç de connexió (#u=…&t=…).
|
|
208
|
+
const pairFrom = (text) => parsePair(text) || parseCode(text);
|
|
194
209
|
function addMachine({ url, token, name }) {
|
|
195
210
|
const ms = store.machines; let i = ms.findIndex((m) => m.url === url);
|
|
196
211
|
if (i >= 0) ms[i].token = token; else { ms.push({ url, token, name: name || new URL(url).hostname }); i = ms.length - 1; }
|
|
@@ -199,7 +214,7 @@ function addMachine({ url, token, name }) {
|
|
|
199
214
|
connect();
|
|
200
215
|
}
|
|
201
216
|
function checkPairHash() {
|
|
202
|
-
const pair = parsePair(location.hash); if (!pair) return false;
|
|
217
|
+
const pair = parsePair(location.hash) || parseCode(location.hash); if (!pair) return false;
|
|
203
218
|
history.replaceState(null, '', location.pathname + '#/sessions');
|
|
204
219
|
state.route = { name: 'sessions' };
|
|
205
220
|
addMachine(pair); render(); toast('Ordinador connectat'); return true;
|
|
@@ -285,9 +300,9 @@ function viewNoMachine(what) {
|
|
|
285
300
|
<div class="panel empty">${bot('sleep')}<h2>Sembla que no hi ha cap sessió iniciada</h2><p>Crea’n una de nova a la pàgina Sessions. Allà connectes l’ordinador i hi dones feina.</p><a class="btn ink" href="#/sessions">Ves a Sessions</a></div>`;
|
|
286
301
|
}
|
|
287
302
|
function pasteForm(open) {
|
|
288
|
-
return `<details class="paste" ${open ? 'open' : ''}><summary>
|
|
289
|
-
<form class="pair-form" id="pairForm"><input type="
|
|
290
|
-
<p class="muted small">Surt al terminal, sota el codi QR.</p></details>`;
|
|
303
|
+
return `<details class="paste" ${open ? 'open' : ''}><summary>Tens un codi?</summary>
|
|
304
|
+
<form class="pair-form" id="pairForm"><input type="text" id="pairInput" placeholder="nio-…" autocomplete="off" autocapitalize="off" spellcheck="false" required aria-label="Codi de connexió"><button class="btn ink" type="submit">Connecta</button></form>
|
|
305
|
+
<p class="muted small">Surt al terminal, sota el codi QR. Copia’l sencer.</p></details>`;
|
|
291
306
|
}
|
|
292
307
|
function viewConnect() {
|
|
293
308
|
const CMD = 'npx nio-cat', cmd = (c) => `<div class="cmd big"><code>${esc(c)}</code><button class="btn sm" data-copy="${esc(c)}" type="button">${svg(ICON.copy)}Copia</button></div>`;
|
|
@@ -312,7 +327,7 @@ function viewConnect() {
|
|
|
312
327
|
<p class="small">Al terminal, des de la carpeta dels teus projectes:</p>${cmd(CMD)}
|
|
313
328
|
<p class="muted small">S’obre aquest panell ja connectat. Cal Node.js 18 o més nou.</p></div></section>
|
|
314
329
|
<section class="panel choice"><header><h2>Un altre ordinador</h2></header><div class="body stack">
|
|
315
|
-
<p class="small">Engega nio a l’altre ordinador o a un servidor. Copia
|
|
330
|
+
<p class="small">Engega nio a l’altre ordinador o a un servidor. Copia aquí el codi que mostra.</p>
|
|
316
331
|
${pasteForm(true)}
|
|
317
332
|
<p class="small"><b>Un servidor (VPS) per SSH.</b> Des d'aquest ordinador, sense obrir cap port:</p>${cmd('npx nio-cat ssh usuari@servidor')}
|
|
318
333
|
<p class="muted small">Engega nio al servidor, obre el túnel i el panell queda connectat.</p></div></section>
|
|
@@ -674,8 +689,8 @@ function addMachineSheet() {
|
|
|
674
689
|
<div class="stack-lg">
|
|
675
690
|
<div class="stack"><b>Un ordinador</b><p class="muted small">Al terminal de l’altre ordinador, des de la carpeta dels projectes:</p>${cmd('npx nio-cat')}<p class="muted small">Escaneja el QR que surt, o enganxa aquí sota l’enllaç.</p></div>
|
|
676
691
|
<div class="stack"><b>Un servidor (VPS) per SSH</b><p class="muted small">Des de l’ordinador on tens aquest panell. No cal obrir cap port al servidor, només Node.js 18 o més nou:</p>${cmd('npx nio-cat ssh usuari@servidor')}<p class="muted small">Engega nio al servidor, obre un túnel SSH i et mostra el panell connectat.</p></div>
|
|
677
|
-
<div class="stack"><b>Un servidor amb adreça pública</b><p class="muted small">Si el servidor té una adreça amb HTTPS (Tailscale, Cloudflare o Caddy):</p>${cmd('npx nio-cat --url https://la-teva-adreça --no-open')}<p class="muted small">Deixa’l sota <span class="mono">tmux</span>, <span class="mono">pm2</span> o <span class="mono">systemd</span>. Copia
|
|
678
|
-
<form class="pair-form" id="pairForm"><input type="
|
|
692
|
+
<div class="stack"><b>Un servidor amb adreça pública</b><p class="muted small">Si el servidor té una adreça amb HTTPS (Tailscale, Cloudflare o Caddy):</p>${cmd('npx nio-cat --url https://la-teva-adreça --no-open')}<p class="muted small">Deixa’l sota <span class="mono">tmux</span>, <span class="mono">pm2</span> o <span class="mono">systemd</span>. Copia aquí el codi que mostra.</p></div>
|
|
693
|
+
<form class="pair-form" id="pairForm"><input type="text" id="pairInput" placeholder="nio-… o enllaç" autocomplete="off" autocapitalize="off" spellcheck="false" required aria-label="Codi o enllaç de connexió"><button class="btn ink" type="submit">Connecta</button></form>
|
|
679
694
|
</div>`);
|
|
680
695
|
}
|
|
681
696
|
let qrTimer = 0;
|
|
@@ -690,7 +705,7 @@ function mobileQrSheet() {
|
|
|
690
705
|
if (n < 8) qrTimer = setTimeout(() => paint(n + 1), 2000);
|
|
691
706
|
return;
|
|
692
707
|
}
|
|
693
|
-
box.innerHTML = `<div class="qr-box">${r.svg}</div><p class="muted small">Escaneja-ho amb la càmera del mòbil.${r.lan ? ' El mòbil ha d’estar a la mateixa wifi.' : ''}</p><button class="btn" type="button" data-copy="${esc(r.
|
|
708
|
+
box.innerHTML = `<div class="qr-box">${r.svg}</div><p class="muted small">Escaneja-ho amb la càmera del mòbil.${r.lan ? ' El mòbil ha d’estar a la mateixa wifi.' : ''}</p><div class="row"><button class="btn" type="button" data-copy="${esc(r.code)}">${svg(ICON.copy)}Copia el codi</button></div><p class="muted small">Si no pots escanejar, enganxa el codi a Sessions → Tria un ordinador.</p>`;
|
|
694
709
|
};
|
|
695
710
|
openSheet(`<h2>Continua al mòbil</h2><div class="stack" id="qrBox"><p class="muted small">Preparant el codi…</p></div><div class="row sheet-foot"><button class="btn ghost" type="button" data-close>Tanca</button></div>`, (sh) => { $('[data-close]', sh).onclick = closeSheet; });
|
|
696
711
|
paint();
|
|
@@ -1017,8 +1032,8 @@ document.addEventListener('submit', (e) => {
|
|
|
1017
1032
|
if (e.target.id === 'pullForm') { e.preventDefault(); return pullModel($('#pullIn').value); }
|
|
1018
1033
|
if (e.target.id !== 'pairForm') return;
|
|
1019
1034
|
e.preventDefault();
|
|
1020
|
-
const pair =
|
|
1021
|
-
if (!pair) return toast('Aquest
|
|
1035
|
+
const pair = pairFrom($('#pairInput', e.target).value);
|
|
1036
|
+
if (!pair) return toast('Aquest codi no és de connexió. Copia’l sencer del terminal.');
|
|
1022
1037
|
closeSheet(); addMachine(pair); toast('Connectant…'); go('#/sessions');
|
|
1023
1038
|
});
|
|
1024
1039
|
|
|
@@ -1211,6 +1226,11 @@ const qs = new URLSearchParams(location.search).get('s');
|
|
|
1211
1226
|
if (qs) history.replaceState(null, '', location.pathname + `#/s/${qs}`);
|
|
1212
1227
|
// Les dades del panell antic (simulació d'agents d'operacions) ja no es fan servir.
|
|
1213
1228
|
try { localStorage.removeItem('nio.panell.v1'); localStorage.removeItem('nio.tour.v1'); } catch { /* res */ }
|
|
1229
|
+
// A la còpia local (127.0.0.1, localhost) la icona té el fons groc suau, per distingir-la de www.nio.cat.
|
|
1230
|
+
if (['127.0.0.1', 'localhost', '[::1]'].includes(location.hostname)) {
|
|
1231
|
+
const ico = document.querySelector('link[rel="icon"]');
|
|
1232
|
+
if (ico) ico.href = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#f6e9a0"/><circle cx="16" cy="16" r="10.2" fill="none" stroke="#272224" stroke-width="3.6"/><rect x="11.9" y="12.6" width="2.6" height="6" rx="1.3" fill="#272224"/><rect x="17.5" y="12.6" width="2.6" height="6" rx="1.3" fill="#272224"/></svg>');
|
|
1233
|
+
}
|
|
1214
1234
|
setNav(false);
|
|
1215
1235
|
if (!checkPairHash()) state.route = parseRoute();
|
|
1216
1236
|
if (machine()) connect();
|
package/web/app/style.css
CHANGED
|
@@ -417,3 +417,5 @@ dialog.sheet.wiz .sheet-in { padding: 0; gap: 0; flex: 1; min-height: 0; display
|
|
|
417
417
|
}
|
|
418
418
|
#fsForm { display: flex; gap: 8px; }
|
|
419
419
|
#fsForm input { flex: 1; min-width: 0; }
|
|
420
|
+
.pair-code { display: grid; gap: 4px; text-align: center; }
|
|
421
|
+
.pair-code b { font-size: 30px; letter-spacing: .14em; font-weight: 600; cursor: copy; }
|