Added custom PWA reloading - 10 minutes of idle or when moved to the background or when loading initially or manually with the new button in settings
CI / build (push) Has been skipped
CI / deploy_prod (push) Successful in 1m19s
CI / deploy_dev (push) Has been skipped

This commit is contained in:
2026-03-04 19:29:18 -06:00
parent 4ebd0b5f61
commit 6a963a32fd
6 changed files with 140 additions and 7 deletions
+110
View File
@@ -0,0 +1,110 @@
import { type Component, createEffect, onCleanup } from 'solid-js';
import { useRegisterSW } from 'virtual:pwa-register/solid';
// We export a function so other parts of the app can manually trigger an update
let manualUpdateFn: (() => void) | undefined;
export const forceServiceWorkerUpdate = () => {
if (manualUpdateFn) {
manualUpdateFn();
} else {
console.warn("PWA Updater not initialized yet");
}
};
export const PwaUpdater: Component = () => {
let registration: ServiceWorkerRegistration | undefined;
const {
needRefresh: [needRefresh],
updateServiceWorker,
} = useRegisterSW({
onRegistered(r: ServiceWorkerRegistration | undefined) {
console.log('SW Registered:', r);
registration = r;
// Periodically check for updates (every 1 hour)
if (r) {
setInterval(() => {
console.log('Periodic SW update check...');
r.update();
}, 60 * 60 * 1000);
}
},
onRegisterError(error: Error | any) {
console.error('SW registration error', error);
},
});
createEffect(() => {
// Expose function globally
manualUpdateFn = () => {
if (needRefresh()) {
console.log('User manually forced an update. Reloading now...');
updateServiceWorker(true);
} else if (registration) {
console.log('User manually asked to check for updates...');
registration.update();
}
};
});
createEffect(() => {
// Check for updates when the app regains focus/visibility
const handleVisibilityFocus = () => {
if (document.visibilityState === 'visible' && registration) {
console.log('App became visible, checking for SW updates...');
registration.update();
}
};
document.addEventListener('visibilitychange', handleVisibilityFocus);
window.addEventListener('focus', handleVisibilityFocus);
onCleanup(() => {
document.removeEventListener('visibilitychange', handleVisibilityFocus);
window.removeEventListener('focus', handleVisibilityFocus);
});
});
createEffect(() => {
if (needRefresh()) {
console.log('New PWA version available. Monitoring for idle/hidden state to update...');
let idleTimeout: number | undefined;
const triggerUpdate = () => {
console.log('Triggering PWA update...');
updateServiceWorker(true);
};
const resetIdle = () => {
if (idleTimeout) window.clearTimeout(idleTimeout);
// 10 minutes idle
idleTimeout = window.setTimeout(triggerUpdate, 10 * 60 * 1000);
};
const handleVisibility = () => {
if (document.visibilityState === 'hidden') {
triggerUpdate();
}
};
// Start idle timer
resetIdle();
// Listen for user activity
const events = ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart'];
events.forEach(e => window.addEventListener(e, resetIdle, { passive: true }));
document.addEventListener('visibilitychange', handleVisibility);
onCleanup(() => {
if (idleTimeout) window.clearTimeout(idleTimeout);
events.forEach(e => window.removeEventListener(e, resetIdle));
document.removeEventListener('visibilitychange', handleVisibility);
});
}
});
return null;
};
+2
View File
@@ -3,12 +3,14 @@ import './index.css'
import App from './App.tsx' import App from './App.tsx'
import { ThemeProvider } from './components/ThemeProvider' import { ThemeProvider } from './components/ThemeProvider'
import { Toaster } from './components/ui/sonner' import { Toaster } from './components/ui/sonner'
import { PwaUpdater } from './components/PwaUpdater'
const root = document.getElementById('root') const root = document.getElementById('root')
render(() => { render(() => {
return ( return (
<ThemeProvider> <ThemeProvider>
<PwaUpdater />
<App /> <App />
<Toaster /> <Toaster />
</ThemeProvider> </ThemeProvider>
+2
View File
@@ -221,11 +221,13 @@ export const startContextPolling = () => {
} }
}; };
createRoot(() => {
createEffect(() => { createEffect(() => {
// Re-run whenever ctx changes // Re-run whenever ctx changes
currentTaskContext(); currentTaskContext();
startContextPolling(); startContextPolling();
}); });
});
export interface FilterTemplate { export interface FilterTemplate {
id: string; id: string;
+18 -1
View File
@@ -77,7 +77,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
</div> </div>
</header> </header>
<div class="flex flex-col gap-12"> <div class="flex flex-col gap-12 mt-4">
{/* Resources Category (Mostly for Mobile) */} {/* Resources Category (Mostly for Mobile) */}
@@ -1086,6 +1086,23 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
</div> </div>
</div> </div>
</div> </div>
<footer class="pt-40 pb-20 flex flex-col items-center gap-4">
<Button
variant="ghost"
size="sm"
class="h-8 gap-2 text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground opacity-30 hover:opacity-100 transition-opacity"
onClick={async () => {
const { forceServiceWorkerUpdate } = await import('@/components/PwaUpdater');
forceServiceWorkerUpdate();
toast.success("Checking for updates...");
}}
>
<Upload class="rotate-180" size={14} />
Check for Updates
</Button>
<p class="text-[0.5rem] font-mono text-muted-foreground/30 uppercase tracking-[0.2em]">Tasgrid v1.0.0</p>
</footer>
</div> </div>
); );
}; };
+3 -1
View File
@@ -10,7 +10,9 @@
"DOM.Iterable" "DOM.Iterable"
], ],
"types": [ "types": [
"vite/client" "vite/client",
"vite-plugin-pwa/client",
"vite-plugin-pwa/solid"
], ],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
solid(), solid(),
tailwindcss(), tailwindcss(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'prompt',
includeAssets: ['icon.webp', 'vite.svg'], includeAssets: ['icon.webp', 'vite.svg'],
manifest: { manifest: {
name: 'Tasgrid', name: 'Tasgrid',