import webPush from 'web-push' import { pool } from '../db.js' let _publicKey = null // VAPID keys: prefer env vars; fall back to auto-generated keys stored in config table. // Keys are generated once on first boot and persist — regenerating would invalidate all // existing subscriptions. export async function ensureVapid() { if (_publicKey) return _publicKey let publicKey = process.env.VAPID_PUBLIC_KEY let privateKey = process.env.VAPID_PRIVATE_KEY const subject = process.env.VAPID_SUBJECT || 'mailto:noreply@localhost' if (!publicKey || !privateKey) { const { rows } = await pool.query("SELECT value FROM config WHERE key = 'vapid_keys'") if (rows.length && rows[0].value?.publicKey) { publicKey = rows[0].value.publicKey privateKey = rows[0].value.privateKey } else { const keys = webPush.generateVAPIDKeys() publicKey = keys.publicKey privateKey = keys.privateKey await pool.query( "INSERT INTO config (key, value) VALUES ('vapid_keys', $1) ON CONFLICT (key) DO UPDATE SET value = $1", [JSON.stringify({ publicKey, privateKey })] ) } } webPush.setVapidDetails(subject, publicKey, privateKey) _publicKey = publicKey return publicKey } export async function sendPushToUser(userEmail, payload) { await ensureVapid() const { rows } = await pool.query( 'SELECT * FROM push_subscriptions WHERE user_email = $1', [userEmail] ) if (!rows.length) return const results = await Promise.allSettled( rows.map(sub => webPush.sendNotification( { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } }, JSON.stringify(payload) ) ) ) // Clean up expired or invalid subscriptions for (let i = 0; i < results.length; i++) { const r = results[i] if (r.status === 'rejected' && [404, 410].includes(r.reason?.statusCode)) { await pool.query('DELETE FROM push_subscriptions WHERE id = $1', [rows[i].id]).catch(() => {}) } } } // targetEmail overrides task.assigned_to (used when reassigning via PATCH before DB update) export function notifyAssignmentPush(task, locationName, targetEmail = null) { const email = targetEmail || (task.assigned_type === 'staff' ? task.assigned_to : null) if (!email) return Promise.resolve() return sendPushToUser(email, { title: `New task: ${task.title}`, body: `${locationName} — ${task.priority} priority`, url: '/maintenance/', tag: `maint-task-${task.id}`, }).catch(err => console.error('[push] notify failed:', err.message)) }