fix(hub): drop hub PWA install to unblock standalone PWAs (#41) #44
4 changed files with 47 additions and 188 deletions
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