nio-cat 0.9.3 → 0.9.5
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 +14 -21
- package/package.json +1 -1
- package/src/control.js +31 -5
- package/src/pair.js +26 -10
- package/web/app/app.js +4 -1
- package/web/index.html +28 -6
- package/web/privadesa.html +10 -1
- package/web/sw.js +1 -1
package/bin/nio.js
CHANGED
|
@@ -111,27 +111,19 @@ async function showPair(publicUrl) {
|
|
|
111
111
|
const { code: secret } = auth.createCode({ secret: true });
|
|
112
112
|
const link = pairLink(app, publicUrl, secret), code = makeCode(publicUrl, secret), qrLink = codeLink(app, code);
|
|
113
113
|
const file = writePairPage(link, code, qrLink);
|
|
114
|
-
// El QR
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
console.log(` Escaneja aquest codi amb la càmera:\n`);
|
|
128
|
-
console.log(terminalQr(qrLink, { margin }));
|
|
129
|
-
console.log('');
|
|
130
|
-
}
|
|
131
|
-
console.log(` ${margin ? 'O copia' : 'Copia'} aquest codi i enganxa'l a Sessions → Tria un ordinador:\n`);
|
|
132
|
-
console.log(` ${code}\n`);
|
|
133
|
-
console.log(c.dim(' Serveix per a un sol dispositiu i caduca en 15 minuts. Per a un altre: nio pair.\n'));
|
|
134
|
-
}
|
|
114
|
+
// El QR sempre al terminal, el més gran que hi càpiga: gran (2 caràcters per mòdul), compacte (mig bloc) i,
|
|
115
|
+
// si ni així hi cap d'ample, cap QR i només el codi. Amb poques files, el marge passa de 2 a 1 mòdul.
|
|
116
|
+
const rows = process.stdout.rows || 999, cols = process.stdout.columns || 999;
|
|
117
|
+
const fits = (m, big) => { const z = terminalQrSize(qrLink, m, big); return z.rows + 4 <= rows && z.cols <= cols; };
|
|
118
|
+
const opt = [[2, true], [2, false], [1, false]].find(([m, big]) => fits(m, big)) || (terminalQrSize(qrLink, 1).cols <= cols ? [1, false] : null);
|
|
119
|
+
console.log(`\n ${c.b('Connecta el mòbil')}\n`);
|
|
120
|
+
if (opt) {
|
|
121
|
+
console.log(` Escaneja aquest codi amb la càmera${panelOpened ? ' (també el tens al navegador)' : ''}:\n`);
|
|
122
|
+
console.log(terminalQr(qrLink, { margin: opt[0], big: opt[1] }));
|
|
123
|
+
} else console.log(` La finestra és massa estreta per al QR${panelOpened ? '; el tens al navegador' : '. Fes-la més ampla i torna-ho a provar: nio pair'}.`);
|
|
124
|
+
console.log(`\n ${opt ? 'O copia' : 'Copia'} aquest codi i enganxa'l a Sessions → Tria un ordinador:\n`);
|
|
125
|
+
console.log(` ${code}\n`);
|
|
126
|
+
console.log(c.dim(' Serveix per a un sol dispositiu i caduca en 15 minuts. Per a un altre: nio pair.\n'));
|
|
135
127
|
if (flags.open) {
|
|
136
128
|
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
137
129
|
try {
|
|
@@ -340,6 +332,7 @@ async function start() {
|
|
|
340
332
|
console.log(' Encara no hi ha cap adreça desada. Engega nio primer.');
|
|
341
333
|
break;
|
|
342
334
|
}
|
|
335
|
+
if (!flags.url) openDashboard();
|
|
343
336
|
await showPair(flags.url || cfg.lastUrl);
|
|
344
337
|
break;
|
|
345
338
|
case 'reset-token': {
|
package/package.json
CHANGED
package/src/control.js
CHANGED
|
@@ -9,7 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const crypto = require('crypto');
|
|
12
|
-
const {
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
13
|
const { which, childEnv } = require('./proc');
|
|
14
14
|
const config = require('./config');
|
|
15
15
|
const { HOME } = config;
|
|
@@ -46,9 +46,32 @@ function classify(command) {
|
|
|
46
46
|
return { tool, kind: KIND[tool], sub, act: !read };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Executa una eina amb un temps màxim que es compleix sempre. Al Mac, agent-browser és un llançador de Node que obre
|
|
50
|
+
// el binari com a fill: matar només el llançador deixava el fill amb la sortida oberta i la petició penjada.
|
|
51
|
+
// Per això: grup de processos propi, s'acaba quan surt el procés (no quan es tanquen les sortides) i, si passa
|
|
52
|
+
// el temps, es mata tot el grup.
|
|
49
53
|
function run(bin, args, timeout = 15000) {
|
|
50
54
|
return new Promise((resolve) => {
|
|
51
|
-
|
|
55
|
+
let out = '', err = '', done = false, child, drain;
|
|
56
|
+
const finish = (ok, why) => {
|
|
57
|
+
if (done) return;
|
|
58
|
+
done = true;
|
|
59
|
+
clearTimeout(timer); clearTimeout(drain);
|
|
60
|
+
child?.stdout?.destroy(); child?.stderr?.destroy();
|
|
61
|
+
resolve({ ok, out, err: err || why || '', timedOut: why === 'temps' });
|
|
62
|
+
};
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { /* ja no hi és */ } }
|
|
65
|
+
finish(false, 'temps');
|
|
66
|
+
}, timeout);
|
|
67
|
+
try {
|
|
68
|
+
child = spawn(bin, args, { env: childEnv(), stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
|
|
69
|
+
} catch (e) { return finish(false, e.message); }
|
|
70
|
+
child.stdout.on('data', (d) => { if (out.length < 8 << 20) out += d; });
|
|
71
|
+
child.stderr.on('data', (d) => { if (err.length < 1 << 20) err += d; });
|
|
72
|
+
child.on('error', (e) => finish(false, e.message));
|
|
73
|
+
child.on('close', (code) => finish(code === 0));
|
|
74
|
+
child.on('exit', (code) => { drain = setTimeout(() => finish(code === 0), 300); });
|
|
52
75
|
});
|
|
53
76
|
}
|
|
54
77
|
|
|
@@ -77,7 +100,9 @@ class Control {
|
|
|
77
100
|
let j = null;
|
|
78
101
|
try { j = JSON.parse(r.out); } catch { /* text */ }
|
|
79
102
|
desk.ready = j ? j.ok !== false : r.ok;
|
|
80
|
-
desk.why = desk.ready ? '' :
|
|
103
|
+
desk.why = desk.ready ? '' : r.timedOut
|
|
104
|
+
? (process.platform === 'darwin' ? 'agent-desktop no respon. Dona-li permís a Configuració del Sistema → Privadesa i seguretat → Accessibilitat i torna-ho a provar.' : 'agent-desktop no respon.')
|
|
105
|
+
: (j?.error?.message || r.err || r.out).split('\n')[0].slice(0, 200);
|
|
81
106
|
}
|
|
82
107
|
const shotTool = process.platform === 'darwin' ? which('screencapture') : which('grim') || which('scrot') || which('gnome-screenshot');
|
|
83
108
|
this.capsCache = {
|
|
@@ -97,7 +122,7 @@ class Control {
|
|
|
97
122
|
const parts = [];
|
|
98
123
|
const tries = kind === 'navegador' ? [['skills', 'get', 'core'], ['skills', 'get', 'agent-browser']] : [['skills', 'get', 'desktop'], ['skills', 'get', 'platform']];
|
|
99
124
|
for (const args of tries) {
|
|
100
|
-
const r = await run(bin, args,
|
|
125
|
+
const r = await run(bin, args, 8000);
|
|
101
126
|
if (r.ok && r.out.trim()) {
|
|
102
127
|
parts.push(r.out.trim().replace(/^---[\s\S]*?\n---\s*/, ''));
|
|
103
128
|
if (kind === 'navegador') break;
|
|
@@ -214,7 +239,8 @@ class Control {
|
|
|
214
239
|
this.audit.add({ type: 'control', what: `${kind === 'navegador' ? 'Control del navegador' : 'Control de l’escriptori'} ${body[kind] ? 'activat' : 'desactivat'}`, by: device?.name });
|
|
215
240
|
}
|
|
216
241
|
config.save(this.cfg);
|
|
217
|
-
|
|
242
|
+
for (const kind of ['navegador', 'escriptori']) caps[kind].enabled = this.enabled(kind);
|
|
243
|
+
return { ...caps, notes };
|
|
218
244
|
},
|
|
219
245
|
},
|
|
220
246
|
{ method: 'POST', path: '/api/screen', handler: ({ body }) => this.screenshot(body?.from) },
|
package/src/pair.js
CHANGED
|
@@ -35,22 +35,38 @@ function qr(text) {
|
|
|
35
35
|
return q;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// QR per al terminal: cada
|
|
39
|
-
//
|
|
40
|
-
function terminalQr(text, { color = !process.env.NO_COLOR, margin = 2 } = {}) {
|
|
38
|
+
// QR per al terminal, compacte: cada caràcter «▀» pinta dos mòduls (dalt amb el color de la lletra, baix amb el del fons).
|
|
39
|
+
// Ocupa la meitat d'alt i d'ample que amb dos espais per mòdul. Sempre negre sobre blanc, amb tema clar o fosc.
|
|
40
|
+
function terminalQr(text, { color = !process.env.NO_COLOR, margin = 2, big = false } = {}) {
|
|
41
|
+
if (big) return terminalQrBig(text, { color, margin });
|
|
41
42
|
const q = qr(text);
|
|
42
43
|
const n = q.getModuleCount();
|
|
43
|
-
const M = margin;
|
|
44
|
+
const M = margin;
|
|
45
|
+
const dark = (r, c) => r >= 0 && c >= 0 && r < n && c < n && q.isDark(r, c);
|
|
46
|
+
const lines = [];
|
|
47
|
+
for (let r = -M; r < n + M; r += 2) {
|
|
48
|
+
let line = '';
|
|
49
|
+
for (let c = -M; c < n + M; c++) {
|
|
50
|
+
const t = dark(r, c), b = r + 1 < n + M ? dark(r + 1, c) : false;
|
|
51
|
+
if (color) line += `\x1b[${t ? 30 : 97};${b ? 40 : 107}m▀`;
|
|
52
|
+
else line += t && b ? ' ' : t ? '▄' : b ? '▀' : '█';
|
|
53
|
+
}
|
|
54
|
+
lines.push(` ${line}${color ? '\x1b[0m' : ''}`);
|
|
55
|
+
}
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// QR gran: dos espais amb color de fons per mòdul. Quadrat i molt fàcil d'escanejar, però ocupa el doble.
|
|
60
|
+
function terminalQrBig(text, { color, margin: M }) {
|
|
61
|
+
const q = qr(text), n = q.getModuleCount();
|
|
44
62
|
const dark = (r, c) => r >= 0 && c >= 0 && r < n && c < n && q.isDark(r, c);
|
|
45
63
|
const lines = [];
|
|
46
64
|
for (let r = -M; r < n + M; r++) {
|
|
47
65
|
let line = '', prev = null;
|
|
48
66
|
for (let c = -M; c < n + M; c++) {
|
|
49
67
|
const d = dark(r, c);
|
|
50
|
-
if (color) {
|
|
51
|
-
|
|
52
|
-
line += ' ';
|
|
53
|
-
} else line += d ? ' ' : '██';
|
|
68
|
+
if (color) { if (d !== prev) line += d ? '\x1b[40m' : '\x1b[107m'; line += ' '; }
|
|
69
|
+
else line += d ? ' ' : '██';
|
|
54
70
|
prev = d;
|
|
55
71
|
}
|
|
56
72
|
lines.push(` ${line}${color ? '\x1b[0m' : ''}`);
|
|
@@ -95,8 +111,8 @@ code{display:block;word-break:break-all;font:12px/1.5 ui-monospace,Menlo,monospa
|
|
|
95
111
|
module.exports = { pairLink, terminalQr, writePairPage, qrSvg, makeCode, codeLink };
|
|
96
112
|
|
|
97
113
|
// Mida del QR al terminal: files i columnes que ocupa amb un marge donat.
|
|
98
|
-
function terminalQrSize(text, margin = 2) {
|
|
114
|
+
function terminalQrSize(text, margin = 2, big = false) {
|
|
99
115
|
const n = qr(text).getModuleCount() + margin * 2;
|
|
100
|
-
return { rows: n, cols: n * 2 + 2 };
|
|
116
|
+
return big ? { rows: n, cols: n * 2 + 2 } : { rows: Math.ceil(n / 2), cols: n + 2 };
|
|
101
117
|
}
|
|
102
118
|
module.exports.terminalQrSize = terminalQrSize;
|
package/web/app/app.js
CHANGED
|
@@ -141,7 +141,9 @@ async function api(path, opts = {}) {
|
|
|
141
141
|
const m = machine();
|
|
142
142
|
if (!m) throw new Error('No hi ha cap ordinador connectat');
|
|
143
143
|
const body = opts.body && typeof opts.body !== 'string' ? opts.body : opts.body ? JSON.parse(opts.body) : undefined;
|
|
144
|
-
|
|
144
|
+
let res;
|
|
145
|
+
try { res = await fetch(m.url + path, { ...opts, headers: { Authorization: `Bearer ${m.token}`, 'content-type': 'application/json', ...(opts.headers || {}) }, body: body ? JSON.stringify(body) : undefined }); }
|
|
146
|
+
catch (e) { if (e.name === 'AbortError') throw e; throw new Error(`No he pogut parlar amb ${m.name || 'l’ordinador'}. Comprova que nio hi estigui engegat i torna-ho a provar.`); }
|
|
145
147
|
const data = await res.json().catch(() => ({}));
|
|
146
148
|
if (!res.ok) throw new Error(data.error || `Error ${res.status}`);
|
|
147
149
|
if (Array.isArray(data.agents)) data.agents = data.agents.filter((x) => x.id !== 'demo'); // l'agent de proves del programa no s'ensenya
|
|
@@ -484,6 +486,7 @@ function renderChrome() {
|
|
|
484
486
|
.map(([k, ic, l, on]) => `<a href="${HASH[k]}" ${on ? 'aria-current="page"' : ''}>${svg(ic)}<span>${l}</span>${k === 'approvals' && n ? `<b class="badge">${n}</b>` : ''}</a>`).join('')
|
|
485
487
|
.replace('</a><a href="#/ordinadors"', `</a><button type="button" data-act="pantalla-tab">${svg('<path d="M5 3l14 7-6 2.2L10.8 19z"/>')}<span>Pantalla</span></button><a href="#/ordinadors"`)
|
|
486
488
|
+ `<button type="button" data-act="more" ${more ? 'aria-current="page"' : ''}>${svg(ICON.more)}<span>Més</span></button>`;
|
|
489
|
+
for (const w of document.querySelectorAll('a.word')) w.setAttribute('href', r.name === 'sessions' ? '/' : '#/sessions');
|
|
487
490
|
const s = r.name === 'session' ? state.sessions.get(r.id) : null;
|
|
488
491
|
$('#here').textContent = s ? s.title || 'Sessió nova' : VIEWS[r.name]?.label || '';
|
|
489
492
|
$('#mRight').innerHTML = r.name === 'session'
|
package/web/index.html
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
<title>nio // els teus agents, des del mòbil</title>
|
|
7
7
|
<meta name="description" content="Claude Code i Codex als teus ordinadors, controlats des del mòbil. Personal, gratuït i amb les teves claus.">
|
|
8
8
|
<link rel="canonical" href="https://www.nio.cat/">
|
|
9
|
+
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
|
10
|
+
<meta name="author" content="f.cat">
|
|
11
|
+
<meta name="application-name" content="nio">
|
|
12
|
+
<link rel="alternate" hreflang="ca" href="https://www.nio.cat/">
|
|
13
|
+
<link rel="alternate" hreflang="x-default" href="https://www.nio.cat/">
|
|
14
|
+
<link rel="alternate" type="text/plain" href="/llms.txt" title="nio per a models de llenguatge">
|
|
15
|
+
<link rel="sitemap" type="application/xml" href="/sitemap.xml">
|
|
9
16
|
<meta name="theme-color" content="#f0ede7">
|
|
10
17
|
<meta property="og:type" content="website">
|
|
11
18
|
<meta property="og:site_name" content="nio">
|
|
@@ -132,6 +139,9 @@ main .own a.btn:hover { color: var(--fg); background: var(--lemon); }
|
|
|
132
139
|
.footer .page { padding: 64px 0 48px; text-align: center; font-size: 13px; color: var(--fg-muted); }
|
|
133
140
|
.footer a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--red); text-decoration-thickness: 1px; text-underline-offset: 3px; }
|
|
134
141
|
.footer a:hover { color: var(--red); }
|
|
142
|
+
.footer .copy-l { margin: 0; }
|
|
143
|
+
.footer .flinks { margin-top: 8px; display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 18px; }
|
|
144
|
+
@media (max-width: 620px) { .footer .page { padding: 48px 0 40px; } .footer .flinks { gap: 6px 18px; } }
|
|
135
145
|
main { padding-bottom: 8px; }
|
|
136
146
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; } }
|
|
137
147
|
|
|
@@ -148,7 +158,6 @@ main { padding-bottom: 8px; }
|
|
|
148
158
|
.page { width: calc(100% - 40px); }
|
|
149
159
|
.head { padding-top: 24px; }
|
|
150
160
|
.mark { font-size: 28px; }
|
|
151
|
-
.hero { padding-top: 56px; }
|
|
152
161
|
section { margin-top: 72px; }
|
|
153
162
|
.sec-head { margin-bottom: 24px; }
|
|
154
163
|
.grid { grid-template-columns: 1fr; gap: 12px; }
|
|
@@ -160,10 +169,23 @@ main { padding-bottom: 8px; }
|
|
|
160
169
|
.panel h3 { font-size: 23px; }
|
|
161
170
|
.rows > div { grid-template-columns: 1fr; gap: 2px; }
|
|
162
171
|
.install { padding-left: 16px; }
|
|
163
|
-
.install code { font-size:
|
|
164
|
-
.own {
|
|
172
|
+
.install code { font-size: 13px; white-space: normal; overflow-wrap: anywhere; text-align: left; line-height: 1.5; padding: 8px 0; }
|
|
173
|
+
.own { border-radius: 22px; }
|
|
174
|
+
.hero { padding-top: 48px; text-align: center; }
|
|
175
|
+
.hero .lead { margin-inline: auto; font-size: 17px; }
|
|
176
|
+
.install { margin-inline: auto; }
|
|
177
|
+
.hero .alt { display: grid; gap: 4px; justify-items: center; text-wrap: balance; }
|
|
178
|
+
.sec-head { text-align: center; margin-inline: auto; }
|
|
179
|
+
.sec-head h2 { font-size: 30px; }
|
|
180
|
+
.step { text-align: left; }
|
|
181
|
+
.own { text-align: center; padding: 28px 18px; }
|
|
182
|
+
.own h2 { margin-bottom: 4px; }
|
|
183
|
+
.own .btns { flex-wrap: nowrap; justify-content: center; gap: 6px; margin-top: 20px; }
|
|
184
|
+
.own .btn { flex: 1 1 auto; justify-content: center; height: 42px; padding: 0 8px; font-size: 13px; white-space: nowrap; }
|
|
165
185
|
}
|
|
186
|
+
@media (max-width: 360px) { .own .btn { font-size: 12px; padding: 0 6px; } }
|
|
166
187
|
</style>
|
|
188
|
+
<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://www.nio.cat/#site","url":"https://www.nio.cat/","name":"nio","inLanguage":"ca","publisher":{"@id":"https://www.nio.cat/#org"}},{"@type":"Organization","@id":"https://www.nio.cat/#org","name":"f.cat","url":"https://f.cat","brand":{"@type":"Brand","name":"nio"},"logo":"https://www.nio.cat/icon-512.png"},{"@type":"SoftwareApplication","@id":"https://www.nio.cat/#app","name":"nio","url":"https://www.nio.cat/","description":"Comandament per a Claude Code, Codex, Grok i Antigravity: els agents treballen als teus ordinadors, amb els teus comptes i les teves claus, i tu els dones feina, els segueixes en directe i aproves el que és arriscat des del mòbil.","applicationCategory":"DeveloperApplication","operatingSystem":"Linux, macOS, web (Android, iOS)","inLanguage":"ca","isAccessibleForFree":true,"offers":{"@type":"Offer","price":"0","priceCurrency":"EUR"},"installUrl":"https://www.nio.cat/install.sh","publisher":{"@id":"https://www.nio.cat/#org"},"image":"https://www.nio.cat/icon-512.png","featureList":["Sessions en directe amb Claude Code, Codex, Grok i Antigravity","Aprovacions amb un toc abans d'editar o executar res","Avisos al mòbil","Feines programades i encadenades","Memòria compartida entre agents","Pantalla remota de l'ordinador","Pissarra amb dibuix assistit","Git i GitHub des del mòbil","Telegram i veu","Ollama i xat local"]},{"@type":"WebPage","@id":"https://www.nio.cat/#page","url":"https://www.nio.cat/","name":"nio // els teus agents, des del mòbil","isPartOf":{"@id":"https://www.nio.cat/#site"},"about":{"@id":"https://www.nio.cat/#app"},"inLanguage":"ca"}]}</script>
|
|
167
189
|
</head>
|
|
168
190
|
<body>
|
|
169
191
|
<header class="page head">
|
|
@@ -176,7 +198,7 @@ main { padding-bottom: 8px; }
|
|
|
176
198
|
<h1>Els teus agents, a les teves màquines, des del mòbil.</h1>
|
|
177
199
|
<p class="lead">nio és un comandament per a Claude Code, Codex i altres agents. Treballen als teus ordinadors, amb els teus comptes i les teves claus. Tu els dones feina des del mòbil, els segueixes en directe i aproves el que és arriscat. Personal i gratuït.</p>
|
|
178
200
|
<div class="install"><code id="cmd">curl -fsSL https://www.nio.cat/install.sh | sh</code><button class="copy" type="button" data-copy="curl -fsSL https://www.nio.cat/install.sh | sh" aria-label="Copia l’ordre"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/></svg><svg class="ok" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></button></div>
|
|
179
|
-
<p class="alt">Linux o Mac
|
|
201
|
+
<p class="alt"><span>Linux o Mac.</span> <span>Per provar-ho sense instal·lar: <code>npx nio-cat</code></span></p>
|
|
180
202
|
</div>
|
|
181
203
|
|
|
182
204
|
<section aria-labelledby="com">
|
|
@@ -212,7 +234,7 @@ main { padding-bottom: 8px; }
|
|
|
212
234
|
<h2 id="teu">Tot és teu</h2>
|
|
213
235
|
</div>
|
|
214
236
|
<div>
|
|
215
|
-
<p>Fas servir els teus agents i les teves claus (Anthropic, OpenAI, ElevenLabs, el bot de Telegram). Les converses, la memòria i les claus es queden als teus ordinadors. nio.cat només serveix l’app i guarda, signada, l’adreça on trobar cada ordinador
|
|
237
|
+
<p>Fas servir els teus agents i les teves claus (Anthropic, OpenAI, ElevenLabs, el bot de Telegram). Les converses, la memòria i les claus es queden als teus ordinadors. nio.cat només serveix l’app i guarda, signada, l’adreça on trobar cada ordinador.</p>
|
|
216
238
|
<div class="btns"><a class="btn" href="/app">Obre el panell</a><a class="btn ghost" href="/qui-soc">Qui sóc</a><a class="btn ghost" href="/novetats">Novetats</a></div>
|
|
217
239
|
</div>
|
|
218
240
|
</div>
|
|
@@ -220,7 +242,7 @@ main { padding-bottom: 8px; }
|
|
|
220
242
|
</main>
|
|
221
243
|
|
|
222
244
|
<footer class="footer">
|
|
223
|
-
<div class="page">© 2026 nio // un projecte de <a href="https://f.cat">f.cat</a
|
|
245
|
+
<div class="page"><p class="copy-l">© 2026 nio // un projecte de <a href="https://f.cat">f.cat</a></p><nav class="flinks" aria-label="Peu"><a href="/privadesa">Privadesa</a></nav></div>
|
|
224
246
|
</footer>
|
|
225
247
|
<script src="/bot.js" defer></script>
|
|
226
248
|
<script>
|
package/web/privadesa.html
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
<title>nio // privadesa</title>
|
|
7
7
|
<meta name="description" content="Què passa per nio.cat i què no hi passa mai.">
|
|
8
8
|
<link rel="canonical" href="https://www.nio.cat/privadesa">
|
|
9
|
+
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
|
10
|
+
<meta name="author" content="f.cat">
|
|
11
|
+
<meta name="application-name" content="nio">
|
|
12
|
+
<link rel="alternate" hreflang="ca" href="https://www.nio.cat/privadesa">
|
|
13
|
+
<link rel="alternate" hreflang="x-default" href="https://www.nio.cat/privadesa">
|
|
9
14
|
<meta name="theme-color" content="#f0ede7">
|
|
10
15
|
<meta property="og:type" content="website">
|
|
11
16
|
<meta property="og:site_name" content="nio">
|
|
@@ -83,6 +88,9 @@ main strong { font-weight: 600; }
|
|
|
83
88
|
.footer .page { padding: 40px 0 56px; text-align: center; font-size: 13px; color: var(--fg-muted); }
|
|
84
89
|
.footer a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--red); text-decoration-thickness: 1px; text-underline-offset: 3px; }
|
|
85
90
|
.footer a:hover { color: var(--red); }
|
|
91
|
+
.footer .copy-l { margin: 0; }
|
|
92
|
+
.footer .flinks { margin-top: 8px; display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 18px; }
|
|
93
|
+
@media (max-width: 620px) { .footer .page { padding: 48px 0 40px; } .footer .flinks { gap: 6px 18px; } }
|
|
86
94
|
|
|
87
95
|
@media (max-width: 620px) {
|
|
88
96
|
body { font-size: 17px; }
|
|
@@ -95,6 +103,7 @@ main strong { font-weight: 600; }
|
|
|
95
103
|
main .cmd code { font-size: 14px; }
|
|
96
104
|
}
|
|
97
105
|
</style>
|
|
106
|
+
<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebPage","@id":"https://www.nio.cat/privadesa#page","url":"https://www.nio.cat/privadesa","name":"nio // privadesa","isPartOf":{"@id":"https://www.nio.cat/#site"},"about":{"@id":"https://www.nio.cat/#app"},"inLanguage":"ca"},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"nio","item":"https://www.nio.cat/"},{"@type":"ListItem","position":2,"name":"Privadesa","item":"https://www.nio.cat/privadesa"}]}]}</script>
|
|
98
107
|
</head>
|
|
99
108
|
<body>
|
|
100
109
|
<header class="page head">
|
|
@@ -141,7 +150,7 @@ main strong { font-weight: 600; }
|
|
|
141
150
|
</main>
|
|
142
151
|
|
|
143
152
|
<footer class="footer">
|
|
144
|
-
<div class="page">© 2026 nio // un projecte de <a href="https://f.cat">f.cat</a
|
|
153
|
+
<div class="page"><p class="copy-l">© 2026 nio // un projecte de <a href="https://f.cat">f.cat</a></p><nav class="flinks" aria-label="Peu"><a href="/">Inici</a><a href="/qui-soc">Qui sóc</a><a href="/novetats">Novetats</a></nav></div>
|
|
145
154
|
</footer>
|
|
146
155
|
<script src="/bot.js" defer></script>
|
|
147
156
|
</body>
|
package/web/sw.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// nio · service worker: obre l'app encara que no hi hagi connexió i mostra els avisos.
|
|
2
|
-
const SHELL = 'nio-shell-
|
|
2
|
+
const SHELL = 'nio-shell-v37';
|
|
3
3
|
const FILES = ['/app', '/app/style.css', '/app/app.js', '/bot.css', '/bot.js', '/board.css', '/board.js', '/favicon.svg', '/manifest.webmanifest'];
|
|
4
4
|
|
|
5
5
|
self.addEventListener('install', (e) => {
|