social overhaul

This commit is contained in:
2026-07-22 15:47:34 -07:00
parent 9db3fa697d
commit 86cda294f9
45 changed files with 3026 additions and 430 deletions
+111
View File
@@ -0,0 +1,111 @@
import webpush from "web-push";
import { vapidConfig, isPushConfigured } from "@/lib/config";
import { pbAdmin } from "@/lib/pocketbase/client";
import { logger } from "@/lib/logger";
import type { PushSubscriptionRecord } from "@/lib/pocketbase/services/push";
export interface PushPayload {
title: string;
body: string;
/** Path (or absolute URL) to open/focus when the notification is clicked. */
url?: string;
/** Optional icon override; defaults to the app icon in the SW handler. */
icon?: string;
/** Optional tag to collapse/replace notifications of the same kind. */
tag?: string;
}
let vapidInitialized = false;
function ensureVapid(): boolean {
if (!isPushConfigured()) {
logger.warn(
"Push | VAPID keys not configured (VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY); skipping send"
);
return false;
}
if (!vapidInitialized) {
webpush.setVapidDetails(
vapidConfig.subject,
vapidConfig.publicKey,
vapidConfig.privateKey
);
vapidInitialized = true;
}
return true;
}
/**
* Low-level: send a single push to one subscription. Throws on transport
* errors. Callers that persist subscriptions should handle 404/410 (dead
* endpoint) themselves — see sendPushToPlayer.
*/
export async function sendPush(
subscription: {
endpoint: string;
keys: { p256dh: string; auth: string };
},
payload: PushPayload
): Promise<void> {
if (!ensureVapid()) return;
await webpush.sendNotification(subscription, JSON.stringify(payload));
}
/**
* Send a push to every device a player has subscribed. Dead subscriptions
* (HTTP 404/410 from the push service) are pruned from storage automatically.
*
* This is the seam that real product triggers will call LATER, e.g.:
* - a match the player is in starts (src/lib/pocketbase/services/matches.ts)
* - a free-agent claim involving the player (src/lib/pocketbase/services/free-agents ...)
* - a badge is awarded to the player (src/lib/pocketbase/services/badges.ts)
* NONE of these are wired yet — this foundation pass only exposes the seam
* plus a manual "test notification" path.
*/
export async function sendPushToPlayer(
playerId: string,
payload: PushPayload
): Promise<{ sent: number; failed: number; pruned: number }> {
if (!ensureVapid()) return { sent: 0, failed: 0, pruned: 0 };
const subscriptions: PushSubscriptionRecord[] =
await pbAdmin.listPushSubscriptionsByPlayer(playerId);
let sent = 0;
let failed = 0;
let pruned = 0;
await Promise.all(
subscriptions.map(async (sub) => {
try {
await sendPush(
{
endpoint: sub.endpoint,
keys: { p256dh: sub.p256dh, auth: sub.auth },
},
payload
);
sent++;
} catch (error: any) {
const status = error?.statusCode;
if (status === 404 || status === 410) {
// Subscription no longer valid — prune it.
try {
await pbAdmin.deletePushSubscriptionById(sub.id);
pruned++;
} catch (delError) {
logger.error("Push | Failed to prune dead subscription", delError);
}
} else {
failed++;
logger.error(
`Push | Failed to send to endpoint (status ${status ?? "?"})`,
error?.body ?? error
);
}
}
})
);
return { sent, failed, pruned };
}