fluxy-bot 0.9.5 → 0.9.6
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 +1 -1
- package/supervisor/index.ts +86 -15
- package/workspace/client/index.html +11 -2
- package/workspace/client/public/sw.js +76 -10
- package/workspace/client/src/App.tsx +80 -1
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
|
})
|
|
@@ -87,7 +158,7 @@ const RECOVERING_HTML = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta
|
|
|
87
158
|
<style>body{background:#0a0a0f;color:#94a3b8;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
88
159
|
div{text-align:center}h1{font-size:18px;margin-bottom:8px;color:#e2e8f0}p{font-size:14px}a{color:#60a5fa}</style></head>
|
|
89
160
|
<body><div><h1>Dashboard is restarting...</h1><p>Refreshing automatically.</p></div>
|
|
90
|
-
<script>setTimeout(()=>location.reload(),3000)</script>
|
|
161
|
+
<script>console.error('[refresh-diag] RECOVERING_HTML loaded — will auto-reload in 3s');setTimeout(()=>{console.error('[refresh-diag] RECOVERING_HTML 3s timer fired — reloading');location.reload()},3000)</script>
|
|
91
162
|
<script src="/fluxy/widget.js"></script></body></html>`;
|
|
92
163
|
|
|
93
164
|
export async function startSupervisor() {
|
|
@@ -33,12 +33,21 @@
|
|
|
33
33
|
<script>
|
|
34
34
|
if('serviceWorker' in navigator){
|
|
35
35
|
navigator.serviceWorker.register('/sw.js').then(function(r){
|
|
36
|
+
console.warn('[refresh-diag] SW registered, state:', r.active?.state, 'waiting:', !!r.waiting);
|
|
36
37
|
r.update();
|
|
37
|
-
if(r.waiting){
|
|
38
|
+
if(r.waiting){
|
|
39
|
+
console.warn('[refresh-diag] SW: found waiting worker — sending SKIP_WAITING');
|
|
40
|
+
r.waiting.postMessage({type:'SKIP_WAITING'});
|
|
41
|
+
}
|
|
38
42
|
r.addEventListener('updatefound',function(){
|
|
43
|
+
console.warn('[refresh-diag] SW: updatefound — new version installing');
|
|
39
44
|
var w=r.installing;
|
|
40
45
|
if(w)w.addEventListener('statechange',function(){
|
|
41
|
-
|
|
46
|
+
console.warn('[refresh-diag] SW installing statechange:', w.state);
|
|
47
|
+
if(w.state==='installed'&&navigator.serviceWorker.controller){
|
|
48
|
+
console.warn('[refresh-diag] SW: new version installed while controller exists — sending SKIP_WAITING');
|
|
49
|
+
w.postMessage({type:'SKIP_WAITING'});
|
|
50
|
+
}
|
|
42
51
|
});
|
|
43
52
|
});
|
|
44
53
|
});
|
|
@@ -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,79 @@ export default function App() {
|
|
|
27
27
|
const [rebuildState, setRebuildState] = useState<'idle' | 'rebuilding' | 'error'>('idle');
|
|
28
28
|
const [buildError, setBuildError] = useState('');
|
|
29
29
|
|
|
30
|
+
// ── Refresh diagnostics ──────────────────────────────────────────
|
|
31
|
+
// Logs every possible reload trigger so we can trace phantom refreshes.
|
|
32
|
+
// Safe to remove once the mystery is solved.
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const t0 = Date.now();
|
|
35
|
+
const tag = `[refresh-diag ${new Date().toISOString()}]`;
|
|
36
|
+
|
|
37
|
+
// 1. Log page load reason
|
|
38
|
+
const navEntries = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
|
|
39
|
+
const navType = navEntries[0]?.type ?? 'unknown'; // 'navigate' | 'reload' | 'back_forward' | 'prerender'
|
|
40
|
+
console.warn(`${tag} Page loaded — navType="${navType}", wasDiscarded=${document.wasDiscarded ?? 'N/A'}, visState="${document.visibilityState}"`);
|
|
41
|
+
|
|
42
|
+
// 2. Intercept location.reload to capture stack trace
|
|
43
|
+
const origReload = location.reload.bind(location);
|
|
44
|
+
location.reload = () => {
|
|
45
|
+
console.error(`${tag} ⚠ location.reload() called! Stack trace:`);
|
|
46
|
+
console.trace();
|
|
47
|
+
origReload();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 3. Service Worker controller changes
|
|
51
|
+
if (navigator.serviceWorker) {
|
|
52
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
53
|
+
console.warn(`${tag} SW controllerchange — new SW took control (can cause reload in some browsers)`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4. Vite HMR events (Vite injects import.meta.hot on the client)
|
|
58
|
+
if (import.meta.hot) {
|
|
59
|
+
// Vite fires 'vite:beforeFullReload' before a full page reload
|
|
60
|
+
import.meta.hot.on('vite:beforeFullReload', (payload: unknown) => {
|
|
61
|
+
console.error(`${tag} ⚠ Vite HMR: full-reload triggered!`, payload);
|
|
62
|
+
console.trace();
|
|
63
|
+
});
|
|
64
|
+
import.meta.hot.on('vite:beforeUpdate', (payload: unknown) => {
|
|
65
|
+
console.warn(`${tag} Vite HMR: beforeUpdate`, payload);
|
|
66
|
+
});
|
|
67
|
+
import.meta.hot.on('vite:error', (payload: unknown) => {
|
|
68
|
+
console.error(`${tag} Vite HMR: error`, payload);
|
|
69
|
+
});
|
|
70
|
+
import.meta.hot.on('vite:ws:disconnect', () => {
|
|
71
|
+
console.warn(`${tag} Vite HMR: WebSocket disconnected`);
|
|
72
|
+
});
|
|
73
|
+
import.meta.hot.on('vite:ws:connect', () => {
|
|
74
|
+
console.warn(`${tag} Vite HMR: WebSocket reconnected (may trigger full-reload if stale)`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 5. Visibility + focus changes (iOS kills PWA processes in background)
|
|
79
|
+
const onVisChange = () => console.warn(`${tag} visibilitychange → "${document.visibilityState}" (uptime: ${((Date.now() - t0) / 1000).toFixed(1)}s)`);
|
|
80
|
+
const onPageShow = (e: PageTransitionEvent) => console.warn(`${tag} pageshow — persisted=${e.persisted} (bfcache restore)`);
|
|
81
|
+
const onFreeze = () => console.warn(`${tag} freeze — page is being frozen by OS`);
|
|
82
|
+
const onResume = () => console.warn(`${tag} resume — page resumed from frozen state`);
|
|
83
|
+
document.addEventListener('visibilitychange', onVisChange);
|
|
84
|
+
window.addEventListener('pageshow', onPageShow);
|
|
85
|
+
document.addEventListener('freeze', onFreeze);
|
|
86
|
+
document.addEventListener('resume', onResume);
|
|
87
|
+
|
|
88
|
+
// 6. beforeunload — last chance to log before the page goes away
|
|
89
|
+
const onBeforeUnload = () => {
|
|
90
|
+
console.warn(`${tag} beforeunload — page is about to unload (uptime: ${((Date.now() - t0) / 1000).toFixed(1)}s)`);
|
|
91
|
+
};
|
|
92
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
document.removeEventListener('visibilitychange', onVisChange);
|
|
96
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
97
|
+
document.removeEventListener('freeze', onFreeze);
|
|
98
|
+
document.removeEventListener('resume', onResume);
|
|
99
|
+
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
100
|
+
};
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
30
103
|
useEffect(() => {
|
|
31
104
|
fetch('/api/settings')
|
|
32
105
|
.then((r) => r.json())
|
|
@@ -41,16 +114,22 @@ export default function App() {
|
|
|
41
114
|
let safetyTimer: ReturnType<typeof setTimeout>;
|
|
42
115
|
const handler = (e: MessageEvent) => {
|
|
43
116
|
if (e.data?.type === 'fluxy:rebuilding') {
|
|
117
|
+
console.warn('[refresh-diag] fluxy:rebuilding received — starting 60s safety timer');
|
|
44
118
|
setRebuildState('rebuilding');
|
|
45
119
|
setBuildError('');
|
|
46
120
|
// Safety: auto-reload after 60s in case app:rebuilt message is lost
|
|
47
121
|
clearTimeout(safetyTimer);
|
|
48
|
-
safetyTimer = setTimeout(() =>
|
|
122
|
+
safetyTimer = setTimeout(() => {
|
|
123
|
+
console.error('[refresh-diag] ⚠ 60s safety timer fired — forcing reload');
|
|
124
|
+
location.reload();
|
|
125
|
+
}, 60_000);
|
|
49
126
|
} else if (e.data?.type === 'fluxy:rebuilt') {
|
|
127
|
+
console.warn('[refresh-diag] fluxy:rebuilt received — reloading now');
|
|
50
128
|
clearTimeout(safetyTimer);
|
|
51
129
|
setRebuildState('idle');
|
|
52
130
|
location.reload();
|
|
53
131
|
} else if (e.data?.type === 'fluxy:build-error') {
|
|
132
|
+
console.warn('[refresh-diag] fluxy:build-error received:', e.data.error);
|
|
54
133
|
clearTimeout(safetyTimer);
|
|
55
134
|
setRebuildState('error');
|
|
56
135
|
setBuildError(e.data.error || 'Build failed');
|