You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

538 lines
14 KiB

<script lang="ts">
import { prewarmCaches, type PrewarmProgress } from '../../services/cache/cache-prewarmer.js';
import { markVersionUpdated, getAppVersion } from '../../services/version-manager.js';
import { getDB } from '../../services/cache/indexeddb-store.js';
import Icon from '../ui/Icon.svelte';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
interface Props {
onComplete: () => void;
}
let { onComplete }: Props = $props();
let updating = $state(false);
let updateComplete = $state(false);
let showPWAUpdatePrompt = $state(false);
let appVersion = $state('0.2.0');
let isPWAInstalled = $state(false);
let progress = $state<PrewarmProgress>({
step: 'Preparing update...',
progress: 0,
total: 7,
current: 0
});
onMount(async () => {
appVersion = await getAppVersion();
// Check if PWA is installed
if (typeof window !== 'undefined') {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
const isIOSStandalone = (window.navigator as any).standalone === true;
isPWAInstalled = isStandalone || isIOSStandalone;
}
});
async function handleUpdate() {
updating = true;
progress = {
step: 'Starting update...',
progress: 0,
total: 7,
current: 0
};
try {
// Step 1: Ensure database is up to date (with timeout)
progress = {
step: 'Updating database structure...',
progress: 5,
total: 8,
current: 1
};
// Add timeout to prevent hanging
await Promise.race([
getDB(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Database update timed out after 10 seconds')), 10000)
)
]).catch(async (error) => {
// If timeout, try to continue anyway (database might still work)
console.warn('Database update timeout, continuing:', error);
try {
// Try to get DB one more time without timeout
await getDB();
} catch (e) {
console.warn('Failed to get database after timeout:', e);
// Continue anyway - the app might still work
}
});
// Step 2: Clean up service workers and caches
progress = {
step: 'Cleaning up service workers...',
progress: 10,
total: 8,
current: 2
};
await cleanupServiceWorkers();
// Step 3: Prewarm caches
await prewarmCaches((prog) => {
progress = {
...prog,
progress: 15 + Math.round((prog.progress * 75) / 100) // Map 0-100 to 15-90
};
});
// Step 4: Mark version as updated
progress = {
step: 'Finalizing update...',
progress: 95,
total: 8,
current: 8
};
await markVersionUpdated();
// Step 5: Complete
progress = {
step: 'Update complete!',
progress: 100,
total: 8,
current: 8
};
// Small delay to show completion
await new Promise(resolve => setTimeout(resolve, 500));
updateComplete = true;
// If PWA is installed, ask if they want to update it
if (isPWAInstalled) {
showPWAUpdatePrompt = true;
} else {
// Not a PWA, route to about page and complete
onComplete();
goto('/about');
}
} catch (error) {
progress = {
step: `Update error: ${error instanceof Error ? error.message : 'Unknown error'}`,
progress: 0,
total: 7,
current: 0
};
console.error('Update failed:', error);
}
}
/**
* Clean up service workers and caches to prevent corrupted content errors
*/
async function cleanupServiceWorkers(): Promise<void> {
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
return;
}
try {
// Get all service worker registrations
const registrations = await navigator.serviceWorker.getRegistrations();
// Unregister all service workers
await Promise.all(
registrations.map(async (registration) => {
try {
// Clear all caches for this registration
if (registration.active) {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
}
// Unregister the service worker
const unregistered = await registration.unregister();
if (unregistered) {
console.log('Service worker unregistered successfully');
}
} catch (error) {
console.warn('Failed to unregister service worker:', error);
// Continue with other registrations even if one fails
}
})
);
// Also try to clear all caches (in case some weren't associated with registrations)
try {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
if (cacheNames.length > 0) {
console.log(`Cleared ${cacheNames.length} cache(s)`);
}
} catch (error) {
console.warn('Failed to clear some caches:', error);
// Non-critical, continue
}
} catch (error) {
console.warn('Service worker cleanup encountered errors (non-critical):', error);
// Non-critical - continue with update even if cleanup fails
}
}
async function handlePWAUpdate() {
try {
// Check for service worker update
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
// Check for updates
await registration.update();
// If there's a waiting service worker, skip waiting and reload
if (registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
// Store route to about page before reload
sessionStorage.setItem('postUpdateRedirect', '/about');
// Reload to apply update
window.location.reload();
return;
}
}
// If no service worker or update mechanism, route to about and reload
sessionStorage.setItem('postUpdateRedirect', '/about');
window.location.reload();
} catch (error) {
console.error('PWA update failed:', error);
// Still reload to ensure update is applied
sessionStorage.setItem('postUpdateRedirect', '/about');
window.location.reload();
}
}
</script>
<div
class="update-modal-overlay"
role="dialog"
aria-modal="true"
aria-labelledby="update-modal-title"
onclick={(e) => e.target === e.currentTarget && !updating && onComplete()}
onkeydown={(e) => {
if (e.key === 'Escape' && !updating) {
onComplete();
}
}}
tabindex="-1"
>
<div class="update-modal">
<div class="update-modal-header">
<h2 id="update-modal-title" class="update-modal-title">New Version Available</h2>
<p class="update-modal-subtitle">
Version {appVersion} is now available. Update to get the latest features and improvements.
</p>
</div>
{#if !updating}
<div class="update-modal-content">
<p class="update-modal-description">
This update will:
</p>
<ul class="update-modal-list">
<li>Update the database structure</li>
<li>Preload common data for faster performance</li>
<li>Prepare caches for quick access</li>
</ul>
</div>
<div class="update-modal-actions">
<button
onclick={handleUpdate}
class="update-button"
aria-label="Update to new version"
>
<Icon name="download" size={16} />
<span>Update Now</span>
</button>
<button
onclick={() => {
onComplete();
goto('/about');
}}
class="update-button-secondary"
aria-label="Update later"
>
Update Later
</button>
</div>
{:else if !updateComplete}
<div class="update-modal-content">
<div class="update-progress">
<div class="update-progress-bar-container">
<div
class="update-progress-bar"
style="width: {progress.progress}%"
></div>
</div>
<p class="update-progress-text">
{progress.step} ({progress.progress}%)
</p>
<p class="update-progress-step">
Step {progress.current} of {progress.total}
</p>
</div>
</div>
{:else if showPWAUpdatePrompt}
<div class="update-modal-content">
<p class="update-modal-description">
The app update is complete! Since you have aitherboard installed as a PWA, would you like to update it now?
</p>
<p class="update-modal-note">
This will reload the app to apply the PWA update.
</p>
</div>
<div class="update-modal-actions">
<button
onclick={handlePWAUpdate}
class="update-button"
aria-label="Update PWA now"
>
<Icon name="download" size={16} />
<span>Update PWA Now</span>
</button>
<button
onclick={() => {
onComplete();
goto('/about');
}}
class="update-button-secondary"
aria-label="Update PWA later"
>
Update Later
</button>
</div>
{/if}
</div>
</div>
<style>
.update-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 1rem;
}
.update-modal {
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.5rem;
max-width: 500px;
width: 100%;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
:global(.dark) .update-modal {
background: var(--fog-dark-post, #334155);
border-color: var(--fog-dark-border, #475569);
}
.update-modal-header {
padding: 1.5rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .update-modal-header {
border-bottom-color: var(--fog-dark-border, #475569);
}
.update-modal-title {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .update-modal-title {
color: var(--fog-dark-text, #f9fafb);
}
.update-modal-subtitle {
margin: 0;
font-size: 0.875rem;
color: var(--fog-text-light, #52667a);
}
:global(.dark) .update-modal-subtitle {
color: var(--fog-dark-text-light, #a8b8d0);
}
.update-modal-content {
padding: 1.5rem;
}
.update-modal-description {
margin: 0 0 1rem 0;
color: var(--fog-text, #1f2937);
}
:global(.dark) .update-modal-description {
color: var(--fog-dark-text, #f9fafb);
}
.update-modal-list {
margin: 0;
padding-left: 1.5rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .update-modal-list {
color: var(--fog-dark-text, #f9fafb);
}
.update-modal-list li {
margin-bottom: 0.5rem;
}
.update-modal-note {
margin: 1rem 0 0 0;
font-size: 0.875rem;
color: var(--fog-text-light, #52667a);
font-style: italic;
}
:global(.dark) .update-modal-note {
color: var(--fog-dark-text-light, #a8b8d0);
}
.update-modal-actions {
padding: 1.5rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
:global(.dark) .update-modal-actions {
border-top-color: var(--fog-dark-border, #475569);
}
.update-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: opacity 0.2s;
}
.update-button:hover {
opacity: 0.9;
}
:global(.dark) .update-button {
background: var(--fog-dark-accent, #94a3b8);
}
.update-button-secondary {
padding: 0.75rem 1.5rem;
background: var(--fog-highlight, #f3f4f6);
color: var(--fog-text, #1f2937);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s;
}
.update-button-secondary:hover {
background: var(--fog-accent, #64748b);
color: white;
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .update-button-secondary {
background: var(--fog-dark-highlight, #475569);
color: var(--fog-dark-text, #f9fafb);
border-color: var(--fog-dark-border, #475569);
}
:global(.dark) .update-button-secondary:hover {
background: var(--fog-dark-accent, #94a3b8);
color: white;
border-color: var(--fog-dark-accent, #94a3b8);
}
.update-progress {
display: flex;
flex-direction: column;
gap: 1rem;
}
.update-progress-bar-container {
width: 100%;
height: 8px;
background: var(--fog-highlight, #f3f4f6);
border-radius: 4px;
overflow: hidden;
}
:global(.dark) .update-progress-bar-container {
background: var(--fog-dark-highlight, #475569);
}
.update-progress-bar {
height: 100%;
background: var(--fog-accent, #64748b);
border-radius: 4px;
transition: width 0.3s ease;
}
:global(.dark) .update-progress-bar {
background: var(--fog-dark-accent, #94a3b8);
}
.update-progress-text {
margin: 0;
font-size: 0.875rem;
font-weight: 500;
color: var(--fog-text, #1f2937);
text-align: center;
}
:global(.dark) .update-progress-text {
color: var(--fog-dark-text, #f9fafb);
}
.update-progress-step {
margin: 0;
font-size: 0.75rem;
color: var(--fog-text-light, #52667a);
text-align: center;
}
:global(.dark) .update-progress-step {
color: var(--fog-dark-text-light, #a8b8d0);
}
</style>