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
package/supervisor/index.ts
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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 (
|
|
78
|
-
if (
|
|
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>
|
|
87
|
-
<style
|
|
88
|
-
div
|
|
89
|
-
<
|
|
90
|
-
<
|
|
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 —
|
|
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
|
-
|
|
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
|
+
}
|