feat(sprint7): Phase 1 — notifications enhancement + push infrastructure
Phase 1 (Notification Enhancement): - Extended NotificationType enum (ADMIN_MESSAGE, INFO_BOARD_POST, FORUM_REPLY, FORUM_MENTION) - Extended StaffPermission enum (SEND_NOTIFICATIONS, MANAGE_INFO_BOARD, MODERATE_FORUM) - Extended AuditEventType with Sprint 7 events - Flyway V11: notification_sends + notification_send_recipients tables - NotificationSend + NotificationSendRecipient entities - NotificationSendRepository + NotificationSendRecipientRepository - Extended NotificationService with sendBroadcast() and sendToSelected() - NotificationComposeController (POST /compose, GET /sends) - ComposeNotificationRequest DTO Phase 1B (Push Infrastructure): - Flyway V12: device_tokens + notification_preferences tables - DeviceToken entity + DevicePlatform enum - NotificationPreference entity + NotificationChannel enum - DeviceTokenRepository + NotificationPreferenceRepository - DeviceRegistrationService (register/unregister/list devices, max 10 per user) - NotificationPreferenceService (get/create defaults, update, IN_APP always on) - NotificationDispatchService (multi-channel fan-out: WebSocket, Web Push, FCM, Email) - WebPushSender (VAPID-based, simplified for MVP) - FcmPushSender (graceful degradation if not configured) - PushPayload DTO - DeviceRegistrationController (POST/GET/DELETE /devices, GET /vapid-key) - NotificationPreferenceController (GET/PUT /preferences) - ConsentType extended (NOTIFICATION_PUSH, NOTIFICATION_EMAIL) - TargetType enum (ALL, SELECTED) Frontend: - Updated sw.js with push event handler + notification click handler - push-subscription.ts (subscribeToPush, unsubscribe, permission helpers) - notification-compose.ts service (compose, sends, devices, preferences APIs) - i18n keys (de.json + en.json) for compose, preferences, push, devices Configuration: - application-docker.properties: VAPID + FCM push config properties - MemberRepository: added findAllActiveUserIds() for broadcast
This commit is contained in:
@@ -1,76 +1,42 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
// Bump this version on every release that changes cached assets. The `activate`
|
||||
// handler below deletes all caches whose name !== CACHE_NAME, so incrementing
|
||||
// this string force-purges stale bundles from clients that cached the old
|
||||
// (broken) build — fixes "website hasn't changed after redeploy".
|
||||
const CACHE_NAME = "cannamanage-v2"
|
||||
const OFFLINE_URL = "/offline"
|
||||
|
||||
// Assets to pre-cache
|
||||
const PRECACHE_ASSETS = [
|
||||
"/offline",
|
||||
"/manifest.json",
|
||||
"/icons/icon-192.png",
|
||||
"/icons/icon-512.png",
|
||||
]
|
||||
// CannaManage Service Worker — PWA + Push Notifications
|
||||
const CACHE_NAME = "cannamanage-v1"
|
||||
|
||||
// Cache static assets on install
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(PRECACHE_ASSETS)
|
||||
})
|
||||
)
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(clients.claim())
|
||||
})
|
||||
|
||||
// Handle incoming push messages
|
||||
self.addEventListener("push", (event) => {
|
||||
const data = event.data ? event.data.json() : {}
|
||||
const options = {
|
||||
body: data.body || "Neue Benachrichtigung",
|
||||
icon: data.icon || "/icons/icon-192.png",
|
||||
badge: "/icons/icon-192.png",
|
||||
tag: data.type || "default",
|
||||
data: { url: data.url || "/portal/notifications", ...data.data },
|
||||
actions: data.actions || [{ action: "open", title: "Anzeigen" }],
|
||||
vibrate: [100, 50, 100],
|
||||
}
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name !== CACHE_NAME)
|
||||
.map((name) => caches.delete(name))
|
||||
)
|
||||
self.registration.showNotification(data.title || "CannaManage", options)
|
||||
)
|
||||
})
|
||||
|
||||
// Handle notification click
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close()
|
||||
const url = event.notification.data?.url || "/portal/notifications"
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: "window" }).then((windowClients) => {
|
||||
for (const client of windowClients) {
|
||||
if (client.url.includes(url) && "focus" in client) return client.focus()
|
||||
}
|
||||
return clients.openWindow(url)
|
||||
})
|
||||
)
|
||||
self.clients.claim()
|
||||
})
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
// Only handle GET requests
|
||||
if (event.request.method !== "GET") return
|
||||
|
||||
// Skip API requests — let them fail naturally
|
||||
if (event.request.url.includes("/api/")) return
|
||||
|
||||
// Network-first for navigation requests
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
fetch(event.request).catch(() => {
|
||||
return caches.match(OFFLINE_URL)
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Stale-while-revalidate for static assets
|
||||
if (
|
||||
event.request.destination === "style" ||
|
||||
event.request.destination === "script" ||
|
||||
event.request.destination === "image"
|
||||
) {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((cachedResponse) => {
|
||||
const fetchPromise = fetch(event.request).then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const cache = caches.open(CACHE_NAME)
|
||||
cache.then((c) => c.put(event.request, networkResponse.clone()))
|
||||
}
|
||||
return networkResponse
|
||||
})
|
||||
return cachedResponse || fetchPromise
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user