nio-cat 0.1.0 → 0.2.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/bin/nio.js +32 -1
- package/package.json +1 -1
- package/src/github.js +144 -0
- package/src/pair.js +5 -1
- package/src/server.js +26 -1
- package/web/app/app.js +1318 -0
- package/web/app/index.html +76 -0
- package/web/app/style.css +381 -0
- package/web/apple-touch-icon.png +0 -0
- package/web/board.css +170 -0
- package/web/board.js +1180 -0
- package/web/bot.css +60 -0
- package/web/bot.js +48 -0
- package/web/favicon.svg +1 -0
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/icon-maskable-512.png +0 -0
- package/web/index.html +140 -0
- package/web/manifest.webmanifest +17 -0
- package/web/og.png +0 -0
- package/web/sw.js +62 -0
package/web/bot.css
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Nio, el bot: una O amb ulls. */
|
|
2
|
+
.bot {
|
|
3
|
+
--s: 1em;
|
|
4
|
+
--bw: .15;
|
|
5
|
+
position: relative;
|
|
6
|
+
display: inline-block;
|
|
7
|
+
flex: none;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
width: var(--s);
|
|
10
|
+
height: var(--s);
|
|
11
|
+
border: calc(var(--s) * var(--bw)) solid currentColor;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
}
|
|
14
|
+
.bot .eyes {
|
|
15
|
+
position: absolute;
|
|
16
|
+
inset: 0;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
gap: calc(var(--s) * .11);
|
|
21
|
+
transform: translate(var(--ex, 0px), var(--ey, 0px));
|
|
22
|
+
transition: transform .18s cubic-bezier(.2, .8, .2, 1);
|
|
23
|
+
}
|
|
24
|
+
.bot .eyes i {
|
|
25
|
+
display: block;
|
|
26
|
+
width: calc(var(--s) * .12);
|
|
27
|
+
height: calc(var(--s) * .22);
|
|
28
|
+
border-radius: 999px;
|
|
29
|
+
background: currentColor;
|
|
30
|
+
transform-origin: center;
|
|
31
|
+
animation: bot-blink 5.4s infinite;
|
|
32
|
+
animation-delay: var(--bd, 0s);
|
|
33
|
+
}
|
|
34
|
+
@keyframes bot-blink { 0%, 94%, 100% { transform: scaleY(1); } 96% { transform: scaleY(.1); } }
|
|
35
|
+
|
|
36
|
+
/* Treballant: mira d'una banda a l'altra */
|
|
37
|
+
.bot[data-mood="work"] .eyes { animation: bot-scan 1.1s ease-in-out infinite alternate; transition: none; }
|
|
38
|
+
@keyframes bot-scan { from { transform: translateX(calc(var(--s) * -.13)); } to { transform: translateX(calc(var(--s) * .13)); } }
|
|
39
|
+
|
|
40
|
+
/* En ombra: observa, amb les parpelles a mitges */
|
|
41
|
+
.bot[data-mood="peek"] .eyes i { height: calc(var(--s) * .12); border-radius: 0 0 999px 999px; }
|
|
42
|
+
|
|
43
|
+
/* Aturat: dorm */
|
|
44
|
+
.bot[data-mood="sleep"] .eyes { transform: translateY(calc(var(--s) * .04)); }
|
|
45
|
+
.bot[data-mood="sleep"] .eyes i { width: calc(var(--s) * .15); height: calc(var(--s) * .035); animation: none; }
|
|
46
|
+
|
|
47
|
+
/* Content: ulls en arc i un saltet */
|
|
48
|
+
.bot[data-mood="happy"] { animation: bot-hop .45s ease 2; }
|
|
49
|
+
.bot[data-mood="happy"] .eyes i { width: calc(var(--s) * .16); height: calc(var(--s) * .09); background: none; border: calc(var(--s) * .045) solid currentColor; border-bottom: 0; border-radius: 999px 999px 0 0; animation: none; }
|
|
50
|
+
@keyframes bot-hop { 50% { transform: translateY(calc(var(--s) * -.12)); } }
|
|
51
|
+
|
|
52
|
+
/* Com a O del nom */
|
|
53
|
+
.word { display: inline-flex; align-items: baseline; }
|
|
54
|
+
/* Mides de la «o» de Manrope 600: 0,547 × 0,578 em, traç 0,111 em (vertical) i 0,099 em (horitzontal),
|
|
55
|
+
comença 0,031 em després de l'origen i baixa 0,016 em sota la línia base. */
|
|
56
|
+
.word .bot { --s: .56em; width: .547em; height: .578em; border-width: .099em .111em; margin-left: .031em; top: .016em; }
|
|
57
|
+
|
|
58
|
+
@media (prefers-reduced-motion: reduce) {
|
|
59
|
+
.bot, .bot * { animation: none !important; transition: none !important; }
|
|
60
|
+
}
|
package/web/bot.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* Nio, el bot: els ulls segueixen el cursor i, sense cursor, de tant en tant fan una ullada. */
|
|
2
|
+
(() => {
|
|
3
|
+
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
4
|
+
const fine = matchMedia('(hover: hover) and (pointer: fine)').matches;
|
|
5
|
+
let px = null, py = null, raf = 0;
|
|
6
|
+
|
|
7
|
+
const free = b => !['work', 'sleep'].includes(b.dataset.mood);
|
|
8
|
+
const delay = b => { if (!b.style.getPropertyValue('--bd')) b.style.setProperty('--bd', (-Math.random() * 5).toFixed(2) + 's'); };
|
|
9
|
+
|
|
10
|
+
function apply() {
|
|
11
|
+
raf = 0;
|
|
12
|
+
document.querySelectorAll('.bot').forEach(b => {
|
|
13
|
+
delay(b);
|
|
14
|
+
const e = b.firstElementChild;
|
|
15
|
+
if (!e) return;
|
|
16
|
+
if (!free(b) || px == null) { e.style.removeProperty('--ex'); e.style.removeProperty('--ey'); return; }
|
|
17
|
+
const r = b.getBoundingClientRect();
|
|
18
|
+
if (!r.width) return;
|
|
19
|
+
const dx = px - (r.left + r.width / 2), dy = py - (r.top + r.height / 2);
|
|
20
|
+
const d = Math.hypot(dx, dy) || 1, k = Math.min(1, d / 260) * r.width * .15;
|
|
21
|
+
e.style.setProperty('--ex', (dx / d * k).toFixed(2) + 'px');
|
|
22
|
+
e.style.setProperty('--ey', (dy / d * k).toFixed(2) + 'px');
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const refresh = () => { if (!raf) raf = requestAnimationFrame(apply); };
|
|
26
|
+
|
|
27
|
+
if (!reduce) {
|
|
28
|
+
addEventListener('pointermove', e => { if (e.pointerType === 'mouse') { px = e.clientX; py = e.clientY; refresh(); } }, { passive: true });
|
|
29
|
+
addEventListener('scroll', refresh, { passive: true });
|
|
30
|
+
document.addEventListener('mouseleave', () => { px = py = null; refresh(); });
|
|
31
|
+
setInterval(() => {
|
|
32
|
+
if (fine && px != null) return;
|
|
33
|
+
document.querySelectorAll('.bot').forEach(b => {
|
|
34
|
+
if (!free(b) || Math.random() < .5) return;
|
|
35
|
+
const e = b.firstElementChild, w = b.getBoundingClientRect().width * .14, a = Math.random() * Math.PI * 2;
|
|
36
|
+
e.style.setProperty('--ex', (Math.cos(a) * w).toFixed(2) + 'px');
|
|
37
|
+
e.style.setProperty('--ey', (Math.sin(a) * w * .6).toFixed(2) + 'px');
|
|
38
|
+
setTimeout(() => { if (px == null || !fine) { e.style.removeProperty('--ex'); e.style.removeProperty('--ey'); } }, 900 + Math.random() * 600);
|
|
39
|
+
});
|
|
40
|
+
}, 3400);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
window.NioBot = {
|
|
44
|
+
html: (mood = 'idle', cls = '') => `<span class="bot ${cls}" data-mood="${mood}" aria-hidden="true" style="--bd:${(-Math.random() * 5).toFixed(2)}s"><span class="eyes"><i></i><i></i></span></span>`,
|
|
45
|
+
refresh
|
|
46
|
+
};
|
|
47
|
+
document.readyState === 'loading' ? addEventListener('DOMContentLoaded', refresh) : refresh();
|
|
48
|
+
})();
|
package/web/favicon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#f0ede7"/><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>
|
package/web/icon-192.png
ADDED
|
Binary file
|
package/web/icon-512.png
ADDED
|
Binary file
|
|
Binary file
|
package/web/index.html
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ca">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>nio // agents al mòbil</title>
|
|
7
|
+
<meta name="description" content="Dona feina als agents d’IA del teu ordinador des del mòbil, segueix-los en directe i aprova cada pas.">
|
|
8
|
+
<link rel="canonical" href="https://www.nio.cat/">
|
|
9
|
+
<meta name="theme-color" content="#f0ede7">
|
|
10
|
+
<meta property="og:type" content="website">
|
|
11
|
+
<meta property="og:site_name" content="nio">
|
|
12
|
+
<meta property="og:locale" content="ca_ES">
|
|
13
|
+
<meta property="og:url" content="https://www.nio.cat/">
|
|
14
|
+
<meta property="og:title" content="nio // agents al mòbil">
|
|
15
|
+
<meta property="og:description" content="Dona feina als agents d’IA del teu ordinador des del mòbil, segueix-los en directe i aprova cada pas.">
|
|
16
|
+
<meta property="og:image" content="https://www.nio.cat/og.png">
|
|
17
|
+
<meta property="og:image:width" content="1200">
|
|
18
|
+
<meta property="og:image:height" content="630">
|
|
19
|
+
<meta property="og:image:alt" content="nio">
|
|
20
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
21
|
+
<meta name="twitter:title" content="nio // agents al mòbil">
|
|
22
|
+
<meta name="twitter:description" content="Dona feina als agents d’IA del teu ordinador des del mòbil, segueix-los en directe i aprova cada pas.">
|
|
23
|
+
<meta name="twitter:image" content="https://www.nio.cat/og.png">
|
|
24
|
+
<meta name="format-detection" content="telephone=no">
|
|
25
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
26
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
27
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
|
28
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
29
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
30
|
+
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
|
|
31
|
+
<link rel="stylesheet" href="/bot.css">
|
|
32
|
+
<style>
|
|
33
|
+
:root {
|
|
34
|
+
--bg: #f0ede7;
|
|
35
|
+
--fg: #272224;
|
|
36
|
+
--fg-muted: #69615c;
|
|
37
|
+
--accent: #f46a3b;
|
|
38
|
+
--border: #cec7bd;
|
|
39
|
+
--lemon: #e7e66a;
|
|
40
|
+
--red: #e2382b;
|
|
41
|
+
--font: "Manrope", system-ui, sans-serif;
|
|
42
|
+
}
|
|
43
|
+
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
|
44
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
45
|
+
body { background: var(--bg); color: var(--fg); font: 400 19px/1.6 var(--font); letter-spacing: -.02em; -webkit-font-smoothing: antialiased; }
|
|
46
|
+
a { color: inherit; }
|
|
47
|
+
::selection { background: var(--lemon); }
|
|
48
|
+
:focus-visible { outline: 2px solid var(--fg); outline-offset: 3px; border-radius: 2px; }
|
|
49
|
+
|
|
50
|
+
.page { width: min(640px, 100% - 48px); margin: 0 auto; }
|
|
51
|
+
.head { padding: 56px 0 0; display: flex; justify-content: space-between; align-items: baseline; }
|
|
52
|
+
.head .enter { display: inline-flex; align-items: center; gap: 8px; height: 34px; padding: 0 14px 0 12px; border: 1px solid var(--border); border-radius: 999px; font-size: 14px; font-weight: 500; color: var(--fg); text-decoration: none; transition: border-color .2s, background-color .2s; }
|
|
53
|
+
.head .enter svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
|
54
|
+
.head .enter:hover { border-color: var(--fg); background: color-mix(in srgb, var(--fg) 4%, transparent); }
|
|
55
|
+
.mark { font-size: 28px; font-weight: 600; letter-spacing: -.07em; line-height: 1; text-decoration: none; }
|
|
56
|
+
|
|
57
|
+
main { padding: 96px 0 96px; }
|
|
58
|
+
h1 { font-size: clamp(36px, 6vw, 52px); font-weight: 500; line-height: 1.06; letter-spacing: -.055em; text-wrap: balance; }
|
|
59
|
+
.lead { margin-top: 28px; font-size: 21px; line-height: 1.5; color: var(--fg-muted); text-wrap: pretty; }
|
|
60
|
+
|
|
61
|
+
section { margin-top: 56px; }
|
|
62
|
+
h2 { font-size: clamp(26px, 3.4vw, 32px); font-weight: 500; line-height: 1.15; letter-spacing: -.045em; margin-bottom: 18px; }
|
|
63
|
+
section p { text-wrap: pretty; }
|
|
64
|
+
section p + p, section p + ul, section ul + p, section p + .cmd, section .cmd + p { margin-top: 1em; }
|
|
65
|
+
main a { text-decoration-thickness: 1px; text-underline-offset: 4px; text-decoration-color: var(--accent); }
|
|
66
|
+
main a:hover { color: var(--accent); }
|
|
67
|
+
main code { font: 500 .88em ui-monospace, "SF Mono", Menlo, monospace; padding: 2px 7px; border-radius: 7px; background: #e6e1d8; }
|
|
68
|
+
main .cmd { display: flex; align-items: center; gap: 4px; padding: 6px 6px 6px 18px; border-radius: 12px; background: #e6e1d8; }
|
|
69
|
+
main .cmd code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; padding: 8px 0; background: none; border-radius: 0; font-size: 16px; scrollbar-width: none; }
|
|
70
|
+
main .cmd code::-webkit-scrollbar { display: none; }
|
|
71
|
+
.copy { flex: none; display: inline-grid; place-items: center; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 9px; background: none; color: var(--fg-muted); cursor: pointer; transition: color .2s, background-color .2s, transform .12s; }
|
|
72
|
+
.copy:hover { color: var(--fg); }
|
|
73
|
+
.copy:active { transform: scale(.97); }
|
|
74
|
+
.copy svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
75
|
+
.copy .ok { display: none; }
|
|
76
|
+
.copy.done .ok { display: block; color: #5b6551; animation: copy-ok .2s cubic-bezier(.22,1,.36,1); }
|
|
77
|
+
@keyframes copy-ok { from { opacity: 0; transform: scale(.6); } }
|
|
78
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none; transition: none; } }
|
|
79
|
+
.copy.done .ic { display: none; }
|
|
80
|
+
main strong { font-weight: 600; }
|
|
81
|
+
|
|
82
|
+
.footer .page { padding: 40px 0 56px; text-align: center; font-size: 13px; color: var(--fg-muted); }
|
|
83
|
+
.footer a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--red); text-decoration-thickness: 1px; text-underline-offset: 3px; }
|
|
84
|
+
.footer a:hover { color: var(--red); }
|
|
85
|
+
|
|
86
|
+
@media (max-width: 620px) {
|
|
87
|
+
body { font-size: 17px; }
|
|
88
|
+
.page { width: calc(100% - 40px); }
|
|
89
|
+
.head { padding-top: 32px; }
|
|
90
|
+
main { padding: 80px 0; }
|
|
91
|
+
.lead { font-size: 19px; }
|
|
92
|
+
section { margin-top: 64px; }
|
|
93
|
+
main .cmd { padding-left: 14px; }
|
|
94
|
+
main .cmd code { font-size: 14px; }
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
97
|
+
</head>
|
|
98
|
+
<body>
|
|
99
|
+
<header class="page head">
|
|
100
|
+
<a class="mark word" href="/" aria-label="Nio, inici">ni<span class="bot" data-mood="idle" aria-hidden="true"><span class="eyes"><i></i><i></i></span></span></a>
|
|
101
|
+
<a class="enter" href="/app"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4"/><path d="M9 16l4-4-4-4"/><path d="M13 12H4"/></svg>Entra al panell</a>
|
|
102
|
+
</header>
|
|
103
|
+
|
|
104
|
+
<main class="page">
|
|
105
|
+
<h1>Els agents d’IA del teu ordinador, al mòbil.</h1>
|
|
106
|
+
<p class="lead">Dona feina a Claude Code, Codex, Grok, Antigravity o Ollama des del mòbil. Treballen al teu ordinador, amb els teus comptes. Tu els segueixes en directe i aproves el que facin. És gratuït.</p>
|
|
107
|
+
|
|
108
|
+
<section aria-labelledby="com">
|
|
109
|
+
<h2 id="com">Com funciona</h2>
|
|
110
|
+
<p>Obre el terminal a la carpeta dels teus projectes i executa:</p>
|
|
111
|
+
<div class="cmd"><code id="cmd">npx nio-cat</code><button class="copy" type="button" data-copy="npx nio-cat" 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>
|
|
112
|
+
<p>Escaneja el codi QR que apareix i ja estàs connectat. A casa, per la wifi; amb <code>cloudflared</code>, des de qualsevol lloc. Només cal tenir <a href="https://nodejs.org">Node.js</a>, i cap dada no passa per servidors nostres.</p>
|
|
113
|
+
</section>
|
|
114
|
+
|
|
115
|
+
<section aria-labelledby="limits">
|
|
116
|
+
<h2 id="limits">Tu decideixes</h2>
|
|
117
|
+
<p>Cada sessió té el seu nivell de confiança: <strong>només llegir</strong>, <strong>demana permís</strong> abans d’editar o executar, o <strong>via lliure</strong>. Quan l’agent et necessita, t’arriba un avís i ho resols amb un toc.</p>
|
|
118
|
+
</section>
|
|
119
|
+
|
|
120
|
+
<section aria-labelledby="prova">
|
|
121
|
+
<h2 id="prova">Prova-ho</h2>
|
|
122
|
+
<p>Sense instal·lar res, amb un ordinador simulat: <a href="/app">obre el panell</a>. També hi trobaràs els teus models d’Ollama i un model petit que funciona dins del mateix mòbil.</p>
|
|
123
|
+
</section>
|
|
124
|
+
</main>
|
|
125
|
+
|
|
126
|
+
<footer class="footer">
|
|
127
|
+
<div class="page">© 2026 nio // un projecte de <a href="https://f.cat">f.cat</a></div>
|
|
128
|
+
</footer>
|
|
129
|
+
<script src="/bot.js" defer></script>
|
|
130
|
+
<script>
|
|
131
|
+
document.addEventListener('click', async (e) => {
|
|
132
|
+
const b = e.target.closest('[data-copy]'); if (!b) return;
|
|
133
|
+
try { await navigator.clipboard.writeText(b.dataset.copy); }
|
|
134
|
+
catch { const r = document.createRange(); r.selectNodeContents(document.getElementById('cmd')); const sel = getSelection(); sel.removeAllRanges(); sel.addRange(r); document.execCommand('copy'); }
|
|
135
|
+
b.classList.add('done');
|
|
136
|
+
clearTimeout(b._t); b._t = setTimeout(() => { b.classList.remove('done'); }, 1800);
|
|
137
|
+
});
|
|
138
|
+
</script>
|
|
139
|
+
</body>
|
|
140
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nio",
|
|
3
|
+
"short_name": "nio",
|
|
4
|
+
"description": "Els agents d’IA del teu ordinador, al mòbil.",
|
|
5
|
+
"lang": "ca",
|
|
6
|
+
"start_url": "/app",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"background_color": "#f0ede7",
|
|
10
|
+
"theme_color": "#f0ede7",
|
|
11
|
+
"icons": [
|
|
12
|
+
{ "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml" },
|
|
13
|
+
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|
14
|
+
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
|
15
|
+
{ "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
|
16
|
+
]
|
|
17
|
+
}
|
package/web/og.png
ADDED
|
Binary file
|
package/web/sw.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// nio · service worker: obre l'app encara que no hi hagi connexió i mostra els avisos.
|
|
2
|
+
const SHELL = 'nio-shell-v2';
|
|
3
|
+
const FILES = ['/app', '/app/style.css', '/app/app.js', '/bot.css', '/bot.js', '/board.css', '/board.js', '/favicon.svg', '/manifest.webmanifest'];
|
|
4
|
+
|
|
5
|
+
self.addEventListener('install', (e) => {
|
|
6
|
+
e.waitUntil(caches.open(SHELL).then((c) => c.addAll(FILES)).catch(() => {}));
|
|
7
|
+
self.skipWaiting();
|
|
8
|
+
});
|
|
9
|
+
self.addEventListener('activate', (e) => {
|
|
10
|
+
e.waitUntil((async () => {
|
|
11
|
+
for (const k of await caches.keys()) if (k.startsWith('nio-shell-') && k !== SHELL) await caches.delete(k);
|
|
12
|
+
await self.clients.claim();
|
|
13
|
+
})());
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Primer la xarxa; si no n'hi ha, la còpia desada.
|
|
17
|
+
self.addEventListener('fetch', (e) => {
|
|
18
|
+
const url = new URL(e.request.url);
|
|
19
|
+
if (e.request.method !== 'GET' || url.origin !== location.origin || url.pathname.startsWith('/api/')) return;
|
|
20
|
+
e.respondWith(
|
|
21
|
+
fetch(e.request)
|
|
22
|
+
.then((res) => {
|
|
23
|
+
if (res.ok && (url.pathname.startsWith('/app') || FILES.includes(url.pathname))) {
|
|
24
|
+
const copy = res.clone();
|
|
25
|
+
caches.open(SHELL).then((c) => c.put(e.request, copy));
|
|
26
|
+
}
|
|
27
|
+
return res;
|
|
28
|
+
})
|
|
29
|
+
.catch(() => caches.match(e.request).then((r) => r || (url.pathname.startsWith('/app') ? caches.match('/app') : undefined)))
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// L'app ens passa l'adreça i la clau de l'ordinador per poder llegir el detall dels avisos.
|
|
34
|
+
self.addEventListener('message', (e) => {
|
|
35
|
+
if (e.data?.type === 'nio-cfg') {
|
|
36
|
+
caches.open('nio-cfg').then((c) => c.put('/cfg', new Response(JSON.stringify({ url: e.data.url, token: e.data.token }))));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Els avisos arriben buits: el detall es demana directament a l'ordinador.
|
|
41
|
+
self.addEventListener('push', (e) => {
|
|
42
|
+
e.waitUntil((async () => {
|
|
43
|
+
let title = 'nio', body = 'Un agent et necessita', session = null;
|
|
44
|
+
try {
|
|
45
|
+
const cfg = await (await (await caches.open('nio-cfg')).match('/cfg')).json();
|
|
46
|
+
const [n] = await (await fetch(`${cfg.url}/api/notices`, { headers: { Authorization: `Bearer ${cfg.token}` } })).json();
|
|
47
|
+
if (n) { title = n.title; body = n.body; session = n.session; }
|
|
48
|
+
} catch { /* sense detall: avís genèric */ }
|
|
49
|
+
await self.registration.showNotification(title, { body, icon: '/icon-192.png', badge: '/icon-192.png', tag: session || 'nio', renotify: true, data: { session } });
|
|
50
|
+
})());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
self.addEventListener('notificationclick', (e) => {
|
|
54
|
+
e.notification.close();
|
|
55
|
+
const session = e.notification.data?.session;
|
|
56
|
+
e.waitUntil((async () => {
|
|
57
|
+
const wins = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
|
58
|
+
const w = wins.find((c) => new URL(c.url).pathname.startsWith('/app'));
|
|
59
|
+
if (w) { await w.focus(); if (session) w.postMessage({ type: 'open-session', id: session }); return; }
|
|
60
|
+
await self.clients.openWindow(session ? `/app#/s/${session}` : '/app');
|
|
61
|
+
})());
|
|
62
|
+
});
|