From 674ee8c6328e177973d4685fa112fe694f9a8197 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sat, 28 Feb 2026 23:12:57 -0600 Subject: [PATCH 1/3] fix: defer new-clip push notification until download succeeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, push notifications for new clips fired immediately after clip creation — before the download even started. If the download failed, users had already been notified about a clip that would never be ready. Now notifications are sent from the download pipeline after status is set to 'ready'. Failed downloads produce no notification. --- src/lib/server/music/download.ts | 6 +++++ src/lib/server/push.ts | 32 ++++++++++++++++++++++++++- src/lib/server/video/download.ts | 6 +++++ src/routes/api/clips/+server.ts | 14 +----------- src/routes/api/clips/share/+server.ts | 14 +----------- 5 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/lib/server/music/download.ts b/src/lib/server/music/download.ts index e8c34b1..4c56172 100644 --- a/src/lib/server/music/download.ts +++ b/src/lib/server/music/download.ts @@ -6,6 +6,7 @@ import { deduplicatedDownload } from '../download-lock'; import { getActiveProvider } from '../providers/registry'; import type { AudioDownloadResult } from '../providers/types'; import { DATA_DIR, getMaxFileSize, cleanupClipFiles } from '$lib/server/download-utils'; +import { notifyNewClip } from '$lib/server/push'; import { createLogger } from '$lib/server/logger'; const log = createLogger('music'); @@ -113,6 +114,11 @@ async function finalizeMusicClip( fileSizeBytes: fileSizeBytes || null }) .where(eq(clips.id, clipId)); + + // Notify group now that the clip is actually ready + await notifyNewClip(clipId).catch((err) => + log.error({ err, clipId }, 'push notification failed') + ); } async function downloadMusicInner(clipId: string, url: string): Promise { diff --git a/src/lib/server/push.ts b/src/lib/server/push.ts index df6a8e0..8979675 100644 --- a/src/lib/server/push.ts +++ b/src/lib/server/push.ts @@ -1,7 +1,7 @@ import webpush from 'web-push'; import { env } from '$env/dynamic/private'; import { db } from '$lib/server/db'; -import { pushSubscriptions, notificationPreferences, users } from '$lib/server/db/schema'; +import { clips, pushSubscriptions, notificationPreferences, users } from '$lib/server/db/schema'; import { eq, inArray } from 'drizzle-orm'; import { createLogger } from '$lib/server/logger'; @@ -62,6 +62,36 @@ export async function sendNotification( ); } +/** + * Send push notification to the group after a clip download succeeds. + * Called from the download pipeline — NOT from the API endpoint. + */ +export async function notifyNewClip(clipId: string): Promise { + const clip = await db.query.clips.findFirst({ + where: eq(clips.id, clipId) + }); + if (!clip) return; + + const uploader = await db.query.users.findFirst({ + where: eq(users.id, clip.addedBy) + }); + if (!uploader) return; + + const label = clip.contentType === 'music' ? 'song' : 'video'; + + await sendGroupNotification( + clip.groupId, + { + title: 'New clip added', + body: `${uploader.username} shared a new ${label}`, + url: '/', + tag: 'new-clip' + }, + 'newAdds', + uploader.id + ); +} + export async function sendGroupNotification( groupId: string, payload: NotificationPayload, diff --git a/src/lib/server/video/download.ts b/src/lib/server/video/download.ts index 4f9ee80..0cedaa2 100644 --- a/src/lib/server/video/download.ts +++ b/src/lib/server/video/download.ts @@ -9,6 +9,7 @@ import { cleanupClipFiles, totalFileSize } from '$lib/server/download-utils'; +import { notifyNewClip } from '$lib/server/push'; import { createLogger } from '$lib/server/logger'; const log = createLogger('video'); @@ -94,6 +95,11 @@ async function downloadVideoInner(clipId: string, url: string): Promise { fileSizeBytes: fileSizeBytes || null }) .where(eq(clips.id, clipId)); + + // Notify group now that the clip is actually ready + await notifyNewClip(clipId).catch((err) => + log.error({ err, clipId }, 'push notification failed') + ); } catch (err) { await handleDownloadError(clipId, err, maxFileSizeBytes); } diff --git a/src/routes/api/clips/+server.ts b/src/routes/api/clips/+server.ts index 159b2de..99c6e4c 100644 --- a/src/routes/api/clips/+server.ts +++ b/src/routes/api/clips/+server.ts @@ -19,7 +19,6 @@ import { } from '$lib/url-validation'; import { downloadVideo } from '$lib/server/video/download'; import { downloadMusic } from '$lib/server/music/download'; -import { sendGroupNotification } from '$lib/server/push'; import { normalizeUrl } from '$lib/server/download-lock'; import { getActiveProvider } from '$lib/server/providers/registry'; import { v4 as uuid } from 'uuid'; @@ -265,18 +264,7 @@ export const POST: RequestHandler = withAuth(async ({ request }, { user, group } downloadVideo(clipId, videoUrl).catch(markFailedOnError); } - // Notify group members - sendGroupNotification( - user.groupId, - { - title: 'New clip added', - body: `${user.username} shared a new ${contentType === 'music' ? 'song' : 'video'}`, - url: '/', - tag: 'new-clip' - }, - 'newAdds', - user.id - ).catch((err) => log.error({ err }, 'push notification failed')); + // Push notification is sent after download succeeds (see video/download.ts, music/download.ts) return json({ clip: { id: clipId, status: 'downloading', contentType } }, { status: 201 }); }); diff --git a/src/routes/api/clips/share/+server.ts b/src/routes/api/clips/share/+server.ts index eb230bc..cdd5379 100644 --- a/src/routes/api/clips/share/+server.ts +++ b/src/routes/api/clips/share/+server.ts @@ -14,7 +14,6 @@ import { } from '$lib/url-validation'; import { downloadVideo } from '$lib/server/video/download'; import { downloadMusic } from '$lib/server/music/download'; -import { sendGroupNotification } from '$lib/server/push'; import { normalizeUrl } from '$lib/server/download-lock'; import { getActiveProvider } from '$lib/server/providers/registry'; import { parseBody, isResponse } from '$lib/server/api-utils'; @@ -145,18 +144,7 @@ export const POST: RequestHandler = async ({ request, url }) => { downloadVideo(clipId, videoUrl).catch(markFailedOnError); } - // 12. Push notification - sendGroupNotification( - group.id, - { - title: 'New clip added', - body: `${matchedUser.username} shared a new ${contentType === 'music' ? 'song' : 'video'}`, - url: '/', - tag: 'new-clip' - }, - 'newAdds', - matchedUser.id - ).catch((err) => log.error({ err }, 'push notification failed')); + // Push notification is sent after download succeeds (see video/download.ts, music/download.ts) return json({ ok: true, clipId, status: 'downloading' }, { status: 201 }); }; From 7d3c9908e5022bc66a6d659a4f63ac815fff81f7 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sat, 28 Feb 2026 23:13:07 -0600 Subject: [PATCH 2/3] docs: remove SMS sharing references, clarify Twilio is verification-only Twilio is only used for phone verification codes, not for video/music ingestion via SMS. Updated all docs to reflect this: CLAUDE.md, architecture, data model, deployment guides, features, notifications, and platform support. --- CLAUDE.md | 2 +- docs/architecture.md | 6 +++--- docs/data-model.md | 4 ++-- docs/deployment/configuration.md | 29 ++++++++++------------------- docs/deployment/manual.md | 12 +++--------- docs/features.md | 6 +++--- docs/guide/features.md | 8 ++++---- docs/guide/platform-support.md | 2 +- docs/index.md | 2 +- docs/notifications.md | 16 +++++++++------- 10 files changed, 37 insertions(+), 50 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f6e41a1..9bfad90 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ Private video-sharing PWA for friend groups. SvelteKit + SQLite + Twilio. - **Backend:** SvelteKit adapter-node (monolith) - **Database:** SQLite via Drizzle ORM - **Styling:** Scoped `