fix(hub): drop hub PWA install to unblock standalone PWAs (#41) #44

Merged
padreug merged 1 commit from fix/issue-41-drop-hub-pwa into main 2026-05-06 06:08:58 +00:00
4 changed files with 47 additions and 188 deletions

View file

@ -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
})
}
})

View 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)
}
}

View file

@ -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()

View file

@ -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: {