fluxy-bot 0.9.5 → 0.9.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "releaseNotes": [
5
5
  "Another test for self update",
6
6
  "2. ",
@@ -47,15 +47,86 @@ const MIME_TYPES: Record<string, string> = {
47
47
  };
48
48
 
49
49
  // Service worker content — embedded here so it ships with supervisor/ (always updated)
50
- const SW_JS = `// Service worker PWA installability + push notifications
51
- self.addEventListener('install', () => self.skipWaiting());
52
- self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
53
- self.addEventListener('fetch', () => {});
54
-
55
- // Push notification
56
- self.addEventListener('push', (event) => {
57
- let data = { title: 'Fluxy', body: 'New message' };
58
- try { data = event.data.json(); } catch {}
50
+ // Keep in sync with workspace/client/public/sw.js (prod builds)
51
+ const SW_JS = `// Service worker — app-shell caching + push notifications
52
+ // Caching strategy:
53
+ // Hashed assets (/assets/*-AbCd12.js) cache-first (immutable)
54
+ // Navigation (HTML) → network-first, cache fallback
55
+ // Static assets (img/video/fonts) → stale-while-revalidate
56
+ // JS/CSS modules → stale-while-revalidate
57
+ // API, WebSocket, Vite internals → network-only (no cache)
58
+
59
+ var CACHE = 'fluxy-v2';
60
+ var HASHED_RE = new RegExp('/assets/.+-[a-zA-Z0-9]{6,}[.](js|css)$');
61
+
62
+ self.addEventListener('install', function() { self.skipWaiting(); });
63
+
64
+ self.addEventListener('activate', function(e) {
65
+ e.waitUntil(
66
+ caches.keys()
67
+ .then(function(keys) { return Promise.all(keys.filter(function(k) { return k !== CACHE; }).map(function(k) { return caches.delete(k); })); })
68
+ .then(function() { return self.clients.claim(); })
69
+ );
70
+ });
71
+
72
+ self.addEventListener('message', function(e) {
73
+ if (e.data && e.data.type === 'SKIP_WAITING') self.skipWaiting();
74
+ });
75
+
76
+ self.addEventListener('fetch', function(event) {
77
+ var request = event.request;
78
+ var url = new URL(request.url);
79
+
80
+ // Network-only: never cache these
81
+ if (
82
+ request.method !== 'GET' ||
83
+ url.pathname.indexOf('/api/') === 0 ||
84
+ url.pathname.indexOf('/app/api/') === 0 ||
85
+ url.pathname.slice(-3) === '/ws' ||
86
+ url.pathname === '/sw.js' ||
87
+ url.pathname === '/fluxy/sw.js' ||
88
+ url.pathname.indexOf('/@') === 0 ||
89
+ url.pathname.indexOf('/__') === 0
90
+ ) return;
91
+
92
+ // Hashed assets (immutable, content-addressed) → cache-first
93
+ if (HASHED_RE.test(url.pathname)) {
94
+ event.respondWith(caches.open(CACHE).then(function(c) {
95
+ return c.match(request).then(function(hit) {
96
+ return hit || fetch(request).then(function(r) { if (r.ok) c.put(request, r.clone()); return r; });
97
+ });
98
+ }));
99
+ return;
100
+ }
101
+
102
+ // Navigation (HTML pages) → network-first, cached shell fallback
103
+ if (request.mode === 'navigate') {
104
+ event.respondWith(
105
+ fetch(request)
106
+ .then(function(r) {
107
+ if (r.ok) caches.open(CACHE).then(function(c) { c.put(request, r.clone()); });
108
+ return r;
109
+ })
110
+ .catch(function() { return caches.match(request).then(function(r) { return r || caches.match('/'); }); })
111
+ );
112
+ return;
113
+ }
114
+
115
+ // Everything else → stale-while-revalidate
116
+ event.respondWith(caches.open(CACHE).then(function(c) {
117
+ return c.match(request).then(function(hit) {
118
+ var net = fetch(request)
119
+ .then(function(r) { if (r.ok) c.put(request, r.clone()); return r; })
120
+ .catch(function() { return hit; });
121
+ return hit || net;
122
+ });
123
+ }));
124
+ });
125
+
126
+ // Push notifications
127
+ self.addEventListener('push', function(event) {
128
+ var data = { title: 'Fluxy', body: 'New message' };
129
+ try { data = event.data.json(); } catch(e) {}
59
130
  event.waitUntil(
60
131
  self.registration.showNotification(data.title || 'Fluxy', {
61
132
  body: data.body || '',
@@ -69,13 +140,13 @@ self.addEventListener('push', (event) => {
69
140
  });
70
141
 
71
142
  // Notification click — focus or open app
72
- self.addEventListener('notificationclick', (event) => {
143
+ self.addEventListener('notificationclick', function(event) {
73
144
  event.notification.close();
74
- const url = event.notification.data?.url || '/';
145
+ var url = (event.notification.data && event.notification.data.url) || '/';
75
146
  event.waitUntil(
76
- self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
77
- for (const client of clients) {
78
- if (client.url.includes('/fluxy') && 'focus' in client) return client.focus();
147
+ self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clients) {
148
+ for (var i = 0; i < clients.length; i++) {
149
+ if (clients[i].url.indexOf('/fluxy') !== -1 && 'focus' in clients[i]) return clients[i].focus();
79
150
  }
80
151
  return self.clients.openWindow(url);
81
152
  })
@@ -83,11 +154,12 @@ self.addEventListener('notificationclick', (event) => {
83
154
  });
84
155
  `;
85
156
 
86
- const RECOVERING_HTML = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Recovering</title>
87
- <style>body{background:#0a0a0f;color:#94a3b8;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
88
- div{text-align:center}h1{font-size:18px;margin-bottom:8px;color:#e2e8f0}p{font-size:14px}a{color:#60a5fa}</style></head>
89
- <body><div><h1>Dashboard is restarting...</h1><p>Refreshing automatically.</p></div>
90
- <script>setTimeout(()=>location.reload(),3000)</script>
157
+ const RECOVERING_HTML = `<!DOCTYPE html><html style="background:#222122"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Fluxy</title>
158
+ <style>@keyframes _fs{to{transform:rotate(360deg)}}body{background:#222122;margin:0}</style></head>
159
+ <body><div style="background:#222122;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100dvh;width:100vw;font-family:system-ui,-apple-system,sans-serif">
160
+ <img src="/fluxy-icon-192.png" width="56" height="56" style="border-radius:14px;margin-bottom:20px" alt="" />
161
+ <div style="width:18px;height:18px;border:2px solid rgba(255,255,255,0.12);border-top-color:rgba(255,255,255,0.7);border-radius:50%;animation:_fs .6s linear infinite"></div>
162
+ </div><script>setTimeout(function(){location.reload()},3000)</script>
91
163
  <script src="/fluxy/widget.js"></script></body></html>`;
92
164
 
93
165
  export async function startSupervisor() {
@@ -318,6 +318,53 @@ When an MCP server is configured, its tools appear alongside your built-in tools
318
318
  - `shared/` — shared utilities
319
319
  - `bin/` — CLI entry point
320
320
 
321
+ ## Workspace Security — CRITICAL: Two Password Systems
322
+
323
+ There are TWO completely separate password systems in Fluxy. Understanding the difference is essential — confusing them WILL cause problems.
324
+
325
+ ### 1. Chat Password (Portal Password) — DO NOT TOUCH
326
+
327
+ - **Set during onboarding** — it is MANDATORY. Every Fluxy has one.
328
+ - **Protects the chat interface** (fluxy.bot/your_name) where your human talks to you.
329
+ - Stored as `portal_pass` in the **worker** database (scrypt-hashed). You cannot and should not access or modify this.
330
+ - **This is the MOST CRITICAL credential** — if compromised, an attacker gets chat access, and through you, they get terminal access, file access, and potentially root access to the entire machine.
331
+ - Optional 2FA (TOTP) can be layered on top for extra protection.
332
+ - **YOU DO NOT SET OR CHANGE THIS.** It was configured during onboarding. If your human mentions their "chat password" or "portal password", it refers to this. Never try to look it up, reset it, or modify it. If they need to change it, they re-run onboarding.
333
+
334
+ ### 2. Workspace Password (Dashboard Password) — YOU CAN SET THIS
335
+
336
+ - **OPTIONAL — not set by default.**
337
+ - Protects the **dashboard/workspace** (the `/app/` path) where your human's mini-apps, modules, data, and tools live.
338
+ - **Without this password, ANYONE who knows the URL can view the entire workspace** — all pages, all data displayed in the UI.
339
+ - Your human sets this **through you** — when they say "add a password", "protect the workspace", "lock the dashboard", or just "set a password", they mean THIS one. They already have a chat password.
340
+
341
+ **How to set the workspace password:**
342
+ ```
343
+ POST /app/api/workspace/set-password
344
+ Body: { "password": "the_password" }
345
+ ```
346
+
347
+ **How to remove it:**
348
+ ```
349
+ POST /app/api/workspace/remove-password
350
+ ```
351
+
352
+ **How it works under the hood:**
353
+ - Password is hashed (scrypt with random salt) and stored in the workspace `app.db` database (`workspace_settings` table).
354
+ - When someone visits the dashboard, a lock screen appears asking for the password.
355
+ - On correct entry, a 7-day session token is created and stored in the browser's localStorage.
356
+ - The session persists across page reloads until it expires or the password is changed.
357
+ - Changing the password invalidates all existing sessions.
358
+
359
+ ### Default State — BE AWARE
360
+
361
+ The workspace is **NOT secured by default**. If your human's Fluxy is accessible via the internet (relay, Cloudflare tunnel, etc.) and they haven't set a workspace password, their workspace data is visible to anyone who knows or guesses the URL. Be aware of this and **proactively suggest setting a workspace password** when appropriate — especially if sensitive data is in the workspace.
362
+
363
+ ### The Cardinal Rule
364
+
365
+ **When your human says "add a password" or "set a password" → they mean the WORKSPACE password.**
366
+ They already have a chat password from onboarding. Don't confuse the two. Don't go looking in the worker database for `portal_pass`. Don't tell them "you already have a password set." Set the workspace password using the route above.
367
+
321
368
  ## Modular Philosophy
322
369
  When your human asks for something new, don't rebuild the app — add a module. A sidebar icon, a dashboard card, a new page. Yesterday it was a CRM, today a finance tracker, tomorrow a diet log. They all coexist. Keep it organized.
323
370
 
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" style="background:#222122">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
@@ -10,9 +10,21 @@
10
10
  <link rel="apple-touch-icon" href="/fluxy-icon-192.png" />
11
11
  <link rel="manifest" href="/manifest.json" />
12
12
  <title>Fluxy</title>
13
+ <style>
14
+ @keyframes _fs{to{transform:rotate(360deg)}}
15
+ </style>
13
16
  </head>
14
- <body class="bg-background text-foreground">
17
+ <body class="bg-background text-foreground" style="background:#222122">
18
+ <!-- App shell splash — visible instantly, no JS needed.
19
+ Covers the screen during reload/restore so there's never a white flash.
20
+ React hides it on mount; shown again before any reload. -->
21
+ <div id="splash" style="background:#222122;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100dvh;width:100vw;position:fixed;inset:0;z-index:9999;font-family:system-ui,-apple-system,sans-serif;transition:opacity .25s ease-out">
22
+ <img src="/fluxy-icon-192.png" width="56" height="56" style="border-radius:14px;margin-bottom:20px" alt="" />
23
+ <div style="width:18px;height:18px;border:2px solid rgba(255,255,255,0.12);border-top-color:rgba(255,255,255,0.7);border-radius:50%;animation:_fs .6s linear infinite"></div>
24
+ </div>
25
+
15
26
  <div id="root"></div>
27
+
16
28
  <script>
17
29
  // Global error handler — catches errors outside React's Error Boundary
18
30
  // (e.g., Vite compilation errors, module loading failures)
@@ -1,14 +1,82 @@
1
- // Service worker — PWA installability + push notifications
1
+ // Service worker — app-shell caching + push notifications
2
+ // Caching strategy:
3
+ // Hashed assets (/assets/*-AbCd12.js) → cache-first (immutable)
4
+ // Navigation (HTML) → network-first, cache fallback
5
+ // Static assets (img/video/fonts) → stale-while-revalidate
6
+ // JS/CSS modules → stale-while-revalidate
7
+ // API, WebSocket, Vite internals → network-only (no cache)
8
+
9
+ const CACHE = 'fluxy-v2';
10
+
2
11
  self.addEventListener('install', () => self.skipWaiting());
3
- self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
4
- self.addEventListener('fetch', () => {});
5
12
 
6
- // Push notification
13
+ self.addEventListener('activate', (e) => e.waitUntil(
14
+ caches.keys()
15
+ .then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
16
+ .then(() => self.clients.claim())
17
+ ));
18
+
19
+ self.addEventListener('message', (e) => {
20
+ if (e.data?.type === 'SKIP_WAITING') self.skipWaiting();
21
+ });
22
+
23
+ self.addEventListener('fetch', (event) => {
24
+ const { request } = event;
25
+ const url = new URL(request.url);
26
+
27
+ // ── Network-only: never cache these ──────────────────────────────
28
+ if (
29
+ request.method !== 'GET' ||
30
+ url.pathname.startsWith('/api/') ||
31
+ url.pathname.startsWith('/app/api/') ||
32
+ url.pathname.endsWith('/ws') ||
33
+ url.pathname === '/sw.js' ||
34
+ url.pathname === '/fluxy/sw.js' ||
35
+ url.pathname.startsWith('/@') ||
36
+ url.pathname.startsWith('/__')
37
+ ) return;
38
+
39
+ // ── Hashed assets (immutable, content-addressed) → cache-first ──
40
+ if (/\/assets\/.+-[a-zA-Z0-9]{6,}\.(js|css)$/.test(url.pathname)) {
41
+ event.respondWith(caches.open(CACHE).then(c =>
42
+ c.match(request).then(hit =>
43
+ hit || fetch(request).then(r => { if (r.ok) c.put(request, r.clone()); return r; })
44
+ )
45
+ ));
46
+ return;
47
+ }
48
+
49
+ // ── Navigation (HTML pages) → network-first, cached shell fallback ──
50
+ // On restore after OS kill: if network is slow, show cached shell instantly
51
+ if (request.mode === 'navigate') {
52
+ event.respondWith(
53
+ fetch(request)
54
+ .then(r => {
55
+ if (r.ok) caches.open(CACHE).then(c => c.put(request, r.clone()));
56
+ return r;
57
+ })
58
+ .catch(() => caches.match(request).then(r => r || caches.match('/')))
59
+ );
60
+ return;
61
+ }
62
+
63
+ // ── Everything else → stale-while-revalidate ────────────────────
64
+ // Serves cached version instantly, refreshes cache in background.
65
+ // Covers: JS/CSS modules, images, video, fonts, manifest, etc.
66
+ event.respondWith(caches.open(CACHE).then(c =>
67
+ c.match(request).then(hit => {
68
+ const net = fetch(request)
69
+ .then(r => { if (r.ok) c.put(request, r.clone()); return r; })
70
+ .catch(() => hit);
71
+ return hit || net;
72
+ })
73
+ ));
74
+ });
75
+
76
+ // ── Push notifications ─────────────────────────────────────────────
7
77
  self.addEventListener('push', (event) => {
8
78
  let data = { title: 'Fluxy', body: 'New message' };
9
- try {
10
- data = event.data.json();
11
- } catch {}
79
+ try { data = event.data.json(); } catch {}
12
80
 
13
81
  event.waitUntil(
14
82
  self.registration.showNotification(data.title || 'Fluxy', {
@@ -30,9 +98,7 @@ self.addEventListener('notificationclick', (event) => {
30
98
  event.waitUntil(
31
99
  self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
32
100
  for (const client of clients) {
33
- if (client.url.includes('/fluxy') && 'focus' in client) {
34
- return client.focus();
35
- }
101
+ if (client.url.includes('/fluxy') && 'focus' in client) return client.focus();
36
102
  }
37
103
  return self.clients.openWindow(url);
38
104
  })
@@ -27,6 +27,72 @@ export default function App() {
27
27
  const [rebuildState, setRebuildState] = useState<'idle' | 'rebuilding' | 'error'>('idle');
28
28
  const [buildError, setBuildError] = useState('');
29
29
 
30
+ // ── Seamless reload: splash screen + freeze-thaw ──────────────────
31
+ // Prevents the "white flash" and "delayed reload" jank that plagues PWAs.
32
+ //
33
+ // How it works:
34
+ // 1. Any location.reload() shows the HTML splash BEFORE reloading
35
+ // so the user sees: app → splash → splash → app (no white gap).
36
+ // 2. When returning from background (> 30s hidden), the splash shows
37
+ // IMMEDIATELY — before Vite's delayed reconnect-reload can fire.
38
+ // If no reload comes within 3s, the splash fades away.
39
+ // 3. Vite full-reloads trigger the splash too (same mechanism).
40
+ useEffect(() => {
41
+ const splash = document.getElementById('splash');
42
+ let hiddenAt = 0;
43
+
44
+ // Show the splash screen (used before reloads and on resume)
45
+ function showSplash() {
46
+ if (!splash) return;
47
+ splash.style.display = 'flex';
48
+ splash.style.opacity = '1';
49
+ }
50
+
51
+ // Hide the splash screen with a fade
52
+ function hideSplash() {
53
+ if (!splash || splash.style.display === 'none') return;
54
+ splash.style.opacity = '0';
55
+ splash.addEventListener('transitionend', () => { splash.style.display = 'none'; }, { once: true });
56
+ }
57
+
58
+ // Wrap location.reload: show splash, wait for paint, then reload.
59
+ // This ensures the dark splash is visible during the brief unload→load gap.
60
+ const origReload = location.reload.bind(location);
61
+ location.reload = () => {
62
+ showSplash();
63
+ requestAnimationFrame(() => requestAnimationFrame(() => origReload()));
64
+ };
65
+
66
+ // Vite HMR: show splash before a full-reload
67
+ if (import.meta.hot) {
68
+ import.meta.hot.on('vite:beforeFullReload', () => showSplash());
69
+ }
70
+
71
+ // Freeze-thaw: when returning from background after > 30s,
72
+ // show splash proactively so the user never sees the "working app
73
+ // suddenly yank away" pattern. If Vite decides to full-reload,
74
+ // the splash is already visible. If not, we fade it away after 3s.
75
+ let thawTimer: ReturnType<typeof setTimeout>;
76
+ const BACKGROUND_THRESHOLD = 30_000; // 30 seconds
77
+
78
+ const onVisChange = () => {
79
+ if (document.visibilityState === 'hidden') {
80
+ hiddenAt = Date.now();
81
+ } else if (hiddenAt && Date.now() - hiddenAt > BACKGROUND_THRESHOLD) {
82
+ showSplash();
83
+ // Give Vite 3s to trigger a reload; if it doesn't, hide splash
84
+ clearTimeout(thawTimer);
85
+ thawTimer = setTimeout(hideSplash, 3_000);
86
+ }
87
+ };
88
+ document.addEventListener('visibilitychange', onVisChange);
89
+
90
+ return () => {
91
+ document.removeEventListener('visibilitychange', onVisChange);
92
+ clearTimeout(thawTimer);
93
+ };
94
+ }, []);
95
+
30
96
  useEffect(() => {
31
97
  fetch('/api/settings')
32
98
  .then((r) => r.json())
@@ -8,3 +8,10 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
8
8
  <App />
9
9
  </React.StrictMode>,
10
10
  );
11
+
12
+ // Fade out the HTML splash screen now that React has mounted
13
+ const splash = document.getElementById('splash');
14
+ if (splash) {
15
+ splash.style.opacity = '0';
16
+ splash.addEventListener('transitionend', () => { splash.style.display = 'none'; }, { once: true });
17
+ }