fix(hub): drop hub PWA install to unblock standalone PWAs (#41) #44
4 changed files with 47 additions and 188 deletions
fix(hub): drop hub PWA install to unblock standalone PWAs (closes #41)
Pre-#41 the hub shipped a Workbox SW with manifest scope `/`, which claimed the entire app.ariege.io origin and made Chrome treat the path-mounted standalones at /libra/, /market/, etc. as sub-areas of the already-installed hub PWA — suppressing the install affordance for each standalone. Drop the VitePWA plugin from the hub entirely. The hub is a tile-grid launcher; users install the standalones they actually use. Add a decommission helper that runs on every hub boot and unregisters any legacy hub SW, so users who installed the old hub PWA get cleaned up automatically. Standalone SWs at deeper scopes are left alone.
commit
0a0769115b
126
public/sw.js
126
public/sw.js
|
|
@ -1,126 +0,0 @@
|
||||||
// Custom service worker for push notifications
|
|
||||||
// This will be merged with Workbox generated SW
|
|
||||||
|
|
||||||
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching'
|
|
||||||
import { clientsClaim, skipWaiting } from 'workbox-core'
|
|
||||||
|
|
||||||
// Precache and route static assets
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST)
|
|
||||||
cleanupOutdatedCaches()
|
|
||||||
|
|
||||||
// Take control of all pages immediately
|
|
||||||
skipWaiting()
|
|
||||||
clientsClaim()
|
|
||||||
|
|
||||||
// Push notification event handler
|
|
||||||
self.addEventListener('push', (event) => {
|
|
||||||
console.log('Push event received:', event)
|
|
||||||
|
|
||||||
let notificationData = {
|
|
||||||
title: 'New Announcement',
|
|
||||||
body: 'You have a new admin announcement',
|
|
||||||
icon: '/pwa-192x192.png',
|
|
||||||
badge: '/pwa-192x192.png',
|
|
||||||
data: {
|
|
||||||
url: '/',
|
|
||||||
timestamp: Date.now()
|
|
||||||
},
|
|
||||||
tag: 'admin-announcement',
|
|
||||||
requireInteraction: true,
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: 'view',
|
|
||||||
title: 'View',
|
|
||||||
icon: '/pwa-192x192.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: 'dismiss',
|
|
||||||
title: 'Dismiss'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse push data if available
|
|
||||||
if (event.data) {
|
|
||||||
try {
|
|
||||||
const pushData = event.data.json()
|
|
||||||
notificationData = {
|
|
||||||
...notificationData,
|
|
||||||
...pushData
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to parse push data:', error)
|
|
||||||
// Use default notification data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
self.registration.showNotification(notificationData.title, notificationData)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Notification click handler
|
|
||||||
self.addEventListener('notificationclick', (event) => {
|
|
||||||
console.log('Notification clicked:', event)
|
|
||||||
|
|
||||||
event.notification.close()
|
|
||||||
|
|
||||||
const action = event.action
|
|
||||||
const notificationData = event.notification.data || {}
|
|
||||||
|
|
||||||
if (action === 'dismiss') {
|
|
||||||
return // Just close the notification
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default action or 'view' action - open the app
|
|
||||||
const urlToOpen = notificationData.url || '/'
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
||||||
.then((clientList) => {
|
|
||||||
// Try to find an existing window with the app
|
|
||||||
for (const client of clientList) {
|
|
||||||
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
|
||||||
client.focus()
|
|
||||||
// Navigate to the notification URL if different
|
|
||||||
if (client.url !== urlToOpen) {
|
|
||||||
client.navigate(urlToOpen)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If no existing window, open a new one
|
|
||||||
if (clients.openWindow) {
|
|
||||||
return clients.openWindow(urlToOpen)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Background sync for offline notification queue (future enhancement)
|
|
||||||
self.addEventListener('sync', (event) => {
|
|
||||||
if (event.tag === 'notification-queue') {
|
|
||||||
event.waitUntil(
|
|
||||||
// Process any queued notifications when back online
|
|
||||||
console.log('Background sync: notification-queue')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Message handler for communication with main app
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
console.log('Service worker received message:', event.data)
|
|
||||||
|
|
||||||
if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
|
|
||||||
const { title, body, data } = event.data.payload
|
|
||||||
|
|
||||||
self.registration.showNotification(title, {
|
|
||||||
body,
|
|
||||||
icon: '/pwa-192x192.png',
|
|
||||||
badge: '/pwa-192x192.png',
|
|
||||||
data,
|
|
||||||
tag: 'manual-notification',
|
|
||||||
requireInteraction: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
38
src/lib/decommission-hub-sw.ts
Normal file
38
src/lib/decommission-hub-sw.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Unregister the legacy hub service worker (closes #41).
|
||||||
|
*
|
||||||
|
* Pre-#41 the hub shipped a Workbox SW at `${origin}/sw.js` with scope `/`,
|
||||||
|
* which claimed the whole origin and blocked Chrome from offering installs
|
||||||
|
* for the path-mounted standalones at /libra/, /market/, etc. The hub is no
|
||||||
|
* longer a PWA; this helper proactively cleans up that stale registration
|
||||||
|
* for users who installed the hub before this change shipped.
|
||||||
|
*
|
||||||
|
* Only touches SWs whose scope is the origin root. Standalone SWs at
|
||||||
|
* /libra/, /market/, etc. live at deeper scopes and are left alone — they
|
||||||
|
* are still legitimate PWAs.
|
||||||
|
*
|
||||||
|
* Idempotent: once the legacy registration is gone, future calls are
|
||||||
|
* cheap no-ops. Safe to leave in place permanently.
|
||||||
|
*/
|
||||||
|
export async function decommissionHubServiceWorker(): Promise<void> {
|
||||||
|
if (!('serviceWorker' in navigator)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const regs = await navigator.serviceWorker.getRegistrations()
|
||||||
|
const originRoot = `${location.origin}/`
|
||||||
|
const legacy = regs.filter(r => r.scope === originRoot)
|
||||||
|
if (legacy.length === 0) return
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
`[decommission-hub-sw] Unregistering ${legacy.length} legacy hub service worker(s).`
|
||||||
|
)
|
||||||
|
await Promise.all(legacy.map(r => r.unregister()))
|
||||||
|
|
||||||
|
if ('caches' in window) {
|
||||||
|
const keys = await caches.keys()
|
||||||
|
await Promise.all(keys.map(k => caches.delete(k)))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[decommission-hub-sw] failed to unregister:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main.ts
17
src/main.ts
|
|
@ -1,24 +1,15 @@
|
||||||
// New modular application entry point
|
// New modular application entry point
|
||||||
import { startApp } from './app'
|
import { startApp } from './app'
|
||||||
import { registerSW } from 'virtual:pwa-register'
|
|
||||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||||
|
import { decommissionHubServiceWorker } from '@/lib/decommission-hub-sw'
|
||||||
import 'vue-sonner/style.css'
|
import 'vue-sonner/style.css'
|
||||||
|
|
||||||
// Clean up any leftover dev-mode service workers from a previous session
|
// Clean up any leftover dev-mode service workers from a previous session
|
||||||
cleanupStaleDevServiceWorkers()
|
cleanupStaleDevServiceWorkers()
|
||||||
|
|
||||||
// Simple periodic service worker updates
|
// Hub is no longer a PWA (#41) — unregister any legacy hub SW left behind
|
||||||
const intervalMS = 60 * 60 * 1000 // 1 hour
|
// on users who installed the old hub PWA before this change shipped.
|
||||||
registerSW({
|
void decommissionHubServiceWorker()
|
||||||
onRegistered(r) {
|
|
||||||
r && setInterval(() => {
|
|
||||||
r.update()
|
|
||||||
}, intervalMS)
|
|
||||||
},
|
|
||||||
onOfflineReady() {
|
|
||||||
console.log('App ready to work offline')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Start the modular application
|
// Start the modular application
|
||||||
startApp()
|
startApp()
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,16 @@ import { fileURLToPath, URL } from 'node:url'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
|
||||||
import Inspect from 'vite-plugin-inspect'
|
import Inspect from 'vite-plugin-inspect'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
|
//
|
||||||
|
// The hub is intentionally NOT a PWA (closes #41). Its scope `/` claimed
|
||||||
|
// the entire origin and blocked Chrome from offering installs for the
|
||||||
|
// path-mounted standalones at /libra/, /market/, etc. The hub is a
|
||||||
|
// launcher page; users install the standalones they actually use.
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
|
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
|
||||||
cacheDir: 'node_modules/.vite-hub',
|
cacheDir: 'node_modules/.vite-hub',
|
||||||
|
|
@ -18,54 +22,6 @@ export default defineConfig(({ mode }) => ({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
VitePWA({
|
|
||||||
registerType: 'autoUpdate',
|
|
||||||
devOptions: {
|
|
||||||
// SW disabled in dev — was caching stale bundles across restarts.
|
|
||||||
// Run `npm run preview` to test PWA behaviour against a real build.
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
// strategies: 'injectManifest',
|
|
||||||
srcDir: 'public',
|
|
||||||
filename: 'sw.js',
|
|
||||||
workbox: {
|
|
||||||
globPatterns: [
|
|
||||||
'**/*.{js,css,html,ico,png,svg}'
|
|
||||||
],
|
|
||||||
// Don't intercept standalone app paths — they have their own service workers
|
|
||||||
navigateFallbackDenylist: [/^\/sortir\//, /^\/libra\//, /^\/wallet\//, /^\/chat\//, /^\/market\//, /^\/cart\//, /^\/checkout\//, /^\/tasks\//, /^\/forum\//, /^\/submit\//, /^\/submission\//],
|
|
||||||
},
|
|
||||||
includeAssets: [
|
|
||||||
'favicon.ico',
|
|
||||||
'apple-touch-icon.png',
|
|
||||||
'mask-icon.svg',
|
|
||||||
// optional: include the icon PNGs explicitly if you also reference them directly
|
|
||||||
'icon-192.png',
|
|
||||||
'icon-512.png',
|
|
||||||
'icon-maskable-192.png',
|
|
||||||
'icon-maskable-512.png',
|
|
||||||
],
|
|
||||||
manifest: {
|
|
||||||
name: 'AIO - Community Hub',
|
|
||||||
short_name: 'AIO',
|
|
||||||
description: 'Nostr-based community platform with Lightning Network integration for events, market and announcements',
|
|
||||||
theme_color: '#1f2937',
|
|
||||||
background_color: '#ffffff',
|
|
||||||
display: 'standalone',
|
|
||||||
orientation: 'portrait-primary',
|
|
||||||
start_url: '/',
|
|
||||||
scope: '/',
|
|
||||||
id: 'aio-community-hub',
|
|
||||||
categories: ['social', 'utilities'],
|
|
||||||
lang: 'en',
|
|
||||||
"icons": [
|
|
||||||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
|
||||||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
|
||||||
{ "src": "/icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
|
|
||||||
{ "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Inspect(),
|
Inspect(),
|
||||||
ViteImageOptimizer({
|
ViteImageOptimizer({
|
||||||
jpg: {
|
jpg: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue