Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ github-actions-*.json
# vercel
.vercel

# PWA (auto-generated by next-pwa)
/public/sw.js
/public/sw.js.map
/public/workbox-*.js
/public/workbox-*.js.map
/public/worker-*.js

# typescript
*.tsbuildinfo
next-env.d.ts
Expand Down
111 changes: 111 additions & 0 deletions app/api/admin/test-push-notification/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import webpush from 'web-push'

Check failure on line 3 in app/api/admin/test-push-notification/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Could not find a declaration file for module 'web-push'. '/home/runner/work/interlinear/interlinear/node_modules/web-push/src/index.js' implicitly has an 'any' type.

export async function POST(request: NextRequest) {

Check warning on line 5 in app/api/admin/test-push-notification/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

'request' is defined but never used
try {
// Configure web-push with VAPID keys at runtime
webpush.setVapidDetails(
`mailto:${process.env.VAPID_CONTACT_EMAIL}`,
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
)

const supabase = await createClient()

// Get current user
const { data: { user }, error: authError } = await supabase.auth.getUser()

if (authError || !user) {
return NextResponse.json(
{ error: 'Unauthorized - must be logged in' },
{ status: 401 }
)
}

// Get user's push subscription
const { data: subscription, error: subError } = await supabase
.from('push_subscriptions')
.select('*')
.eq('user_id', user.id)
.single()

if (subError || !subscription) {
return NextResponse.json(
{ error: 'No push subscription found. Please enable notifications first.' },
{ status: 404 }
)
}

// Parse nested subscription JSONB structure
const subData = subscription.subscription as {
endpoint: string
keys: { p256dh: string; auth: string }
}

const pushSubscription = {
endpoint: subData.endpoint,
keys: {
p256dh: subData.keys.p256dh,
auth: subData.keys.auth
}
}

// Create test notification payload
const notificationPayload = JSON.stringify({
title: 'Test Notification',
body: 'Push notifications are working correctly!',
icon: '/icon-192x192.svg',
badge: '/icon-192x192.svg',
tag: 'test-notification',
data: {
url: '/profile',
type: 'test',
timestamp: Date.now()
},
actions: [
{
action: 'open',
title: 'Open App',
icon: '/icon-192x192.svg'
},
{
action: 'close',
title: 'Dismiss'
}
]
})

// Send the test notification
await webpush.sendNotification(pushSubscription, notificationPayload)

console.log(`Test notification sent to user ${user.id}`)

return NextResponse.json({
success: true,
message: 'Test notification sent successfully'
})

} catch (error: any) {

Check failure on line 89 in app/api/admin/test-push-notification/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
console.error('Test notification error:', error)

// Handle specific web-push errors
if (error.statusCode === 410 || error.statusCode === 404) {
return NextResponse.json(
{
error: 'Push subscription is no longer valid. Please re-enable notifications.',
code: 'SUBSCRIPTION_EXPIRED'
},
{ status: 410 }
)
}

return NextResponse.json(
{
error: 'Failed to send test notification',
message: error.message || 'Unknown error'
},
{ status: 500 }
)
}
}
8 changes: 5 additions & 3 deletions app/api/cron/daily-notifications/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import webpush from 'web-push';

Check failure on line 3 in app/api/cron/daily-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Could not find a declaration file for module 'web-push'. '/home/runner/work/interlinear/interlinear/node_modules/web-push/src/index.js' implicitly has an 'any' type.

export async function GET(request: NextRequest) {
try {
Expand Down Expand Up @@ -96,11 +96,13 @@

// 4. Send notifications to all subscribers
const sendPromises = subscriptions.map(async (sub) => {
// Parse nested subscription JSONB structure
const subData = sub.subscription as { endpoint: string; keys: { p256dh: string; auth: string } };
const pushSubscription = {
endpoint: sub.endpoint,
endpoint: subData.endpoint,
keys: {
p256dh: sub.p256dh_key,
auth: sub.auth_key
p256dh: subData.keys.p256dh,
auth: subData.keys.auth
}
};

Expand All @@ -108,7 +110,7 @@
await webpush.sendNotification(pushSubscription, notificationPayload);
console.log(`✅ Sent to user ${sub.user_id}`);
return { success: true, userId: sub.user_id };
} catch (error: any) {

Check failure on line 113 in app/api/cron/daily-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
console.error(`❌ Failed to send to user ${sub.user_id}:`, error.message);

// Clean up invalid subscriptions
Expand Down
54 changes: 38 additions & 16 deletions app/api/cron/daily-word-generation/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import webpush from 'web-push';

Check failure on line 3 in app/api/cron/daily-word-generation/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Could not find a declaration file for module 'web-push'. '/home/runner/work/interlinear/interlinear/node_modules/web-push/src/index.js' implicitly has an 'any' type.

export async function GET(request: NextRequest) {
try {
Expand Down Expand Up @@ -28,22 +28,24 @@

const today = new Date().toISOString().split('T')[0];

// Step 1: Generate Words for Both Languages
// Step 1: Generate Words for All Languages
console.log('📚 Generating daily words...');

const generateSpanish = await generateWordOfDay('spanish', today, request.nextUrl.origin);
const generateLatin = await generateWordOfDay('latin', today, request.nextUrl.origin);
const generateIcelandic = await generateWordOfDay('icelandic', today, request.nextUrl.origin);

const generationResults = {
spanish: generateSpanish,
latin: generateLatin,
icelandic: generateIcelandic,
date: today
};

// Step 2: Send Teaser Notifications
console.log('🔔 Sending teaser notifications...');

const notificationResults = await sendTeaserNotifications();
const notificationResults = await sendTeaserNotifications(supabase);

return NextResponse.json({
success: true,
Expand All @@ -68,7 +70,7 @@
}
}

async function generateWordOfDay(language: 'spanish' | 'latin', date: string, origin: string) {
async function generateWordOfDay(language: 'spanish' | 'latin' | 'icelandic', date: string, origin: string) {
try {
console.log(`🎲 Generating ${language} word for ${date}`);

Expand Down Expand Up @@ -99,7 +101,7 @@
}
}

async function sendTeaserNotifications() {
async function sendTeaserNotifications(supabase: any) {

Check failure on line 104 in app/api/cron/daily-word-generation/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
try {
// Get all users with notification preferences
const { data: usersWithNotifications, error: prefError } = await supabase
Expand All @@ -124,7 +126,7 @@
console.log(`📤 Sending teaser notifications to ${usersWithNotifications.length} users`);

// Get push subscriptions for users with notifications enabled
const userIds = usersWithNotifications.map(u => u.user_id);

Check failure on line 129 in app/api/cron/daily-word-generation/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Parameter 'u' implicitly has an 'any' type.

const { data: subscriptions, error: subError } = await supabase
.from('push_subscriptions')
Expand All @@ -141,21 +143,39 @@
}

// Create language-specific teaser notifications
const sendPromises = subscriptions.map(async (sub) => {

Check failure on line 146 in app/api/cron/daily-word-generation/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Parameter 'sub' implicitly has an 'any' type.
const userPrefs = usersWithNotifications.find(u => u.user_id === sub.user_id);

Check failure on line 147 in app/api/cron/daily-word-generation/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Parameter 'u' implicitly has an 'any' type.

if (!userPrefs) {
return { success: false, userId: sub.user_id, error: 'User preferences not found' };
}

const isSpanish = userPrefs.preferred_language === 'spanish';
const title = isSpanish
? '📚 ¡Tu palabra del día está lista!'
: '📚 Verbum diei paratum est!';

const body = isSpanish
? 'Descubre una nueva palabra española fascinante'
: 'Discover a fascinating Latin word today';
const isLatin = userPrefs.preferred_language === 'latin';

Check warning on line 154 in app/api/cron/daily-word-generation/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

'isLatin' is assigned a value but never used
const isIcelandic = userPrefs.preferred_language === 'icelandic';

let title: string;
let body: string;
let actionTitle: string;
let closeTitle: string;

if (isSpanish) {
title = '📚 ¡Tu palabra del día está lista!';
body = 'Descubre una nueva palabra española fascinante';
actionTitle = 'Ver Palabra';
closeTitle = 'Cerrar';
} else if (isIcelandic) {
title = '📚 Orðið dagsins er tilbúið!';
body = 'Uppgötvaðu nýtt íslenskt orð í dag';
actionTitle = 'Sjá orð';
closeTitle = 'Loka';
} else {
// Latin or default
title = '📚 Verbum diei paratum est!';
body = 'Discover a fascinating Latin word today';
actionTitle = 'See Word';
closeTitle = 'Close';
}

const notificationPayload = JSON.stringify({
title,
Expand All @@ -172,21 +192,23 @@
actions: [
{
action: 'open',
title: isSpanish ? 'Ver Palabra' : 'See Word',
title: actionTitle,
icon: '/icon-192x192.svg'
},
{
action: 'close',
title: isSpanish ? 'Cerrar' : 'Close'
title: closeTitle
}
]
});

// Parse nested subscription JSONB structure
const subData = sub.subscription as { endpoint: string; keys: { p256dh: string; auth: string } };
const pushSubscription = {
endpoint: sub.endpoint,
endpoint: subData.endpoint,
keys: {
p256dh: sub.p256dh_key,
auth: sub.auth_key
p256dh: subData.keys.p256dh,
auth: subData.keys.auth
}
};

Expand All @@ -194,7 +216,7 @@
await webpush.sendNotification(pushSubscription, notificationPayload);
console.log(`✅ Sent teaser to user ${sub.user_id} (${userPrefs.preferred_language})`);
return { success: true, userId: sub.user_id, language: userPrefs.preferred_language };
} catch (error: any) {

Check failure on line 219 in app/api/cron/daily-word-generation/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
console.error(`❌ Failed to send teaser to user ${sub.user_id}:`, error.message);

// Clean up invalid subscriptions
Expand Down
32 changes: 22 additions & 10 deletions app/api/cron/welcome-notifications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import webpush from 'web-push';

Check failure on line 8 in app/api/cron/welcome-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Could not find a declaration file for module 'web-push'. '/home/runner/work/interlinear/interlinear/node_modules/web-push/src/index.js' implicitly has an 'any' type.

export async function GET(request: NextRequest) {
try {
Expand Down Expand Up @@ -51,7 +51,7 @@
if (userError) {
console.error('❌ Error fetching recent users:', userError);
// Try alternative approach with RPC call
return await sendWelcomeNotificationsAlternative(oneHourFifteenMinutesAgo, oneHourAgo);
return await sendWelcomeNotificationsAlternative(oneHourFifteenMinutesAgo, oneHourAgo, supabase);
}

if (!recentUsers || recentUsers.length === 0) {
Expand Down Expand Up @@ -87,7 +87,7 @@
}

// Send welcome notifications
const welcomeResults = await sendWelcomeNotifications(subscriptions, recentUsers);
const welcomeResults = await sendWelcomeNotifications(subscriptions, recentUsers, supabase);

return NextResponse.json({
success: true,
Expand All @@ -109,7 +109,11 @@
}
}

async function sendWelcomeNotifications(subscriptions: any[], users: any[]) {
async function sendWelcomeNotifications(
subscriptions: any[],

Check failure on line 113 in app/api/cron/welcome-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
users: any[],

Check failure on line 114 in app/api/cron/welcome-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
supabase: any

Check failure on line 115 in app/api/cron/welcome-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
) {
const sendPromises = subscriptions.map(async (sub) => {
const user = users.find(u => u.id === sub.user_id);

Expand Down Expand Up @@ -142,11 +146,13 @@
]
});

// Parse nested subscription JSONB structure
const subData = sub.subscription as { endpoint: string; keys: { p256dh: string; auth: string } };
const pushSubscription = {
endpoint: sub.endpoint,
endpoint: subData.endpoint,
keys: {
p256dh: sub.p256dh_key,
auth: sub.auth_key
p256dh: subData.keys.p256dh,
auth: subData.keys.auth
}
};

Expand All @@ -159,7 +165,7 @@
email: user.email,
signupTime: user.created_at
};
} catch (error: any) {

Check failure on line 168 in app/api/cron/welcome-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
console.error(`❌ Failed to send welcome to user ${sub.user_id}:`, error.message);

// Clean up invalid subscriptions
Expand Down Expand Up @@ -198,7 +204,11 @@
}

// Alternative approach if direct auth.users access fails
async function sendWelcomeNotificationsAlternative(startTime: Date, endTime: Date) {
async function sendWelcomeNotificationsAlternative(
startTime: Date,
endTime: Date,
supabase: any

Check failure on line 210 in app/api/cron/welcome-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
) {
try {
console.log('🔄 Using alternative approach for welcome notifications');

Expand Down Expand Up @@ -238,7 +248,7 @@
}
}

async function sendWelcomeNotificationsSimple(subscriptions: any[]) {

Check failure on line 251 in app/api/cron/welcome-notifications/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Unexpected any. Specify a different type
const sendPromises = subscriptions.map(async (sub) => {
const welcomeNotification = JSON.stringify({
title: '🎉 Welcome to your language adventure!',
Expand All @@ -261,11 +271,13 @@
]
});

// Parse nested subscription JSONB structure
const subData = sub.subscription as { endpoint: string; keys: { p256dh: string; auth: string } };
const pushSubscription = {
endpoint: sub.endpoint,
endpoint: subData.endpoint,
keys: {
p256dh: sub.p256dh_key,
auth: sub.auth_key
p256dh: subData.keys.p256dh,
auth: subData.keys.auth
}
};

Expand Down
25 changes: 22 additions & 3 deletions app/api/generate-word-of-day/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { generateSpanishWordOfDay, generateLatinWordOfDay } from '@/lib/mastra/tools/wordOfDay';
import { generateSpanishWordOfDay, generateLatinWordOfDay, generateIcelandicWordOfDay } from '@/lib/mastra/tools/wordOfDay';

export async function POST(request: NextRequest) {
try {
Expand All @@ -10,11 +10,11 @@
process.env.SUPABASE_SERVICE_ROLE_KEY!
);

const MERRIAM_WEBSTER_API_KEY = process.env.MERRIAM_WEBSTER_API_KEY;

Check warning on line 13 in app/api/generate-word-of-day/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

'MERRIAM_WEBSTER_API_KEY' is assigned a value but never used
const { language = 'spanish', date = new Date().toISOString().split('T')[0] } = await request.json();

if (!['spanish', 'latin'].includes(language)) {
return NextResponse.json({ error: 'Invalid language. Use "spanish" or "latin"' }, { status: 400 });
if (!['spanish', 'latin', 'icelandic'].includes(language)) {
return NextResponse.json({ error: 'Invalid language. Use "spanish", "latin", or "icelandic"' }, { status: 400 });
}

console.log(`🎲 Generating Word of Day for ${language} on ${date}`);
Expand All @@ -41,6 +41,9 @@
if (language === 'spanish') {
// Spanish: Use MW API + LLM workflow
wordData = await generateSpanishWord(date);
} else if (language === 'icelandic') {
// Icelandic: Full LLM generation workflow
wordData = await generateIcelandicWord(date);
} else {
// Latin: Full LLM generation workflow
wordData = await generateLatinWord(date);
Expand All @@ -52,9 +55,9 @@
.insert({
date,
language,
word: wordData.word,

Check failure on line 58 in app/api/generate-word-of-day/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

'wordData' is possibly 'undefined'.
pronunciation: wordData.pronunciation,

Check failure on line 59 in app/api/generate-word-of-day/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

'wordData' is possibly 'undefined'.
part_of_speech: wordData.partOfSpeech,

Check failure on line 60 in app/api/generate-word-of-day/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

'wordData' is possibly 'undefined'.
definitions: wordData.definitions,
static_content: wordData.staticContent, // Etymology, usage notes (not examples)
source_data: wordData.sourceData
Expand Down Expand Up @@ -119,14 +122,30 @@
}
}

async function generateIcelandicWord(date: string) {
try {
// Use direct function call (bypass hanging workflow)
const result = await generateIcelandicWordOfDay({ date });

if (!result.success) {
throw new Error(result.error || 'Icelandic word generation failed');
}

return result.data;
} catch (error) {
console.error('❌ Error in generateIcelandicWord:', error);
throw error;
}
}

// Helper functions for MW API data extraction
function extractPronunciation(mwData: any): string | null {

Check warning on line 142 in app/api/generate-word-of-day/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

'extractPronunciation' is defined but never used
if (!mwData || !Array.isArray(mwData) || mwData.length === 0) return null;
const entry = mwData[0];
return entry.hwi?.prs?.[0]?.mw || null;
}

function extractPartOfSpeech(mwData: any): string | null {

Check warning on line 148 in app/api/generate-word-of-day/route.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

'extractPartOfSpeech' is defined but never used
if (!mwData || !Array.isArray(mwData) || mwData.length === 0) return null;
const entry = mwData[0];
return entry.fl || null;
Expand Down
Loading
Loading