diff --git a/build.pid b/build.pid new file mode 100644 index 0000000..f12111e --- /dev/null +++ b/build.pid @@ -0,0 +1 @@ +2331610 diff --git a/clear-data.ts b/clear-data.ts new file mode 100644 index 0000000..ff3e301 --- /dev/null +++ b/clear-data.ts @@ -0,0 +1,27 @@ +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); + +async function main() { + const user = await prisma.user.findUnique({ + where: { email: 'martin.bierschenk@gmail.com' } + }); + + if (!user) return; + + // Delete all tasks in the target range created today + const res = await prisma.task.deleteMany({ + where: { + userId: user.id, + scheduledDate: { + gte: new Date('2026-03-15T00:00:00Z'), + lte: new Date('2026-04-15T00:00:00Z') + }, + createdAt: { + gte: new Date(new Date().setHours(0,0,0,0)) + } + } + }); + + console.log(`Deleted ${res.count} tasks from martin`); +} +main().finally(() => prisma.$disconnect()); diff --git a/fill-data-v2.ts b/fill-data-v2.ts new file mode 100644 index 0000000..f5dba2b --- /dev/null +++ b/fill-data-v2.ts @@ -0,0 +1,214 @@ +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); + +const dailyTasks = [ + "Pretend to listen to partner's work drama", + "Feed the kids (again? really?)", + "Hide Amazon packages from spouse", + "Attempt to fold a fitted sheet, give up and roll it into a ball", + "Cook a meal that exactly 0% of the family will appreciate", + "Look at the mess. Sigh. Walk away.", + "Move the laundry from washer to dryer, leave it there for 3 days", + "Find out what died in the fridge", + "Pay bills and cry softly", + "Unload dishwasher with immense resentment", + "Scroll on phone until legs go numb on the toilet", + "Perform archeological dig in the teenage bedroom", + "Interrogate child about missing tupperware lid", + "Nod enthusiastically at toddler's incomprehensible story", + "Try to decipher spouse's 'helpful' grocery list", + "Pretend I don't see the full trash can", + "Vacuum the rug and ignore the corners", + "Wonder where all my money went" +]; + +const workEvents = [ + "Meaningless sync meeting #42", + "Stare blankly at spreadsheet", + "Listen to boss talk about synergy", + "Reply 'As per my previous email...' to Gary", + "Pretend to be busy so no one asks me for help", + "'Quick chat' that ruins my entire afternoon", + "Update Jira tickets to make it look like I did something", + "Mute mic and eat aggressively loud chips during all-hands", + "Consider moving to the woods and becoming a hermit", + "Frantically search for the tab that is playing music", + "Draft angry email, delete it, send 'Sounds good!'", + "Nod meaningfully during presentation I don't understand" +]; + +const birthdayTasks = [ + "Buy a gift that makes me look thoughtful but was actually on sale", + "Wrap the present (using newspaper because I forgot wrapping paper)", + "Attend birthday party and strategically position myself near the snack table", + "Fake a smile while listening to Uncle Bob's views", + "Smuggle leftover cake home in napkins" +]; + +async function main() { + const user = await prisma.user.findUnique({ + where: { email: 'kugelblitz@gmx.de' } + }); + + if (!user) return; + + // 1. Delete all recently generated tasks + await prisma.task.deleteMany({ + where: { + userId: user.id, + createdAt: { gte: new Date(new Date().setHours(0,0,0,0)) }, + title: { not: 'Meeting with team' } // leave the one subagent added if you want, or just let it delete if the title is strictly dailyTasks. Actually just delete all created today! + } + }); + + // 2. Create Projects for beautiful colors + let pWork = await prisma.project.findFirst({ where: { userId: user.id, name: 'Work' } }); + if (!pWork) pWork = await prisma.project.create({ data: { userId: user.id, name: 'Work', color: '#3b82f6', icon: 'faBriefcase' } }); + + let pFamily = await prisma.project.findFirst({ where: { userId: user.id, name: 'Family' } }); + if (!pFamily) pFamily = await prisma.project.create({ data: { userId: user.id, name: 'Family', color: '#10b981', icon: 'faHouse' } }); + + let pLife = await prisma.project.findFirst({ where: { userId: user.id, name: 'Life' } }); + if (!pLife) pLife = await prisma.project.create({ data: { userId: user.id, name: 'Life', color: '#f59e0b', icon: 'faHeart' } }); + + // 3. Find target Someday lists to populate the bottom + const lists = await prisma.somedayList.findMany({ where: { userId: user.id } }); + const kugelList = lists.find(l => l.title.includes('kugel')); + const papaList = lists.find(l => l.title.includes('Papa')); + + // Target dates: March 16 to April 12, 2026 + const startDate = new Date('2026-03-16T00:00:00Z'); + const endDate = new Date('2026-04-12T00:00:00Z'); + const birthdayDate = new Date('2026-04-04T00:00:00Z'); + const aprilFools = new Date('2026-04-01T00:00:00Z'); + + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const isBirthday = d.getTime() === birthdayDate.getTime(); + const isAprilFools = d.getTime() === aprilFools.getTime(); + const dateStr = d.toISOString().split('T')[0]; + const isWeekend = d.getDay() === 0 || d.getDay() === 6; + + // We want a DENSE calendar for the tutorial. 50% fill rate for EACH SLOT. + // Daily Life events (scheduled on grid) + const numLife = Math.floor(Math.random() * 2) + 1; + for (let i = 0; i < numLife; i++) { + const h = Math.floor(Math.random() * 3) + 6; // 6-8 AM or evening + await prisma.task.create({ + data: { + title: dailyTasks[Math.floor(Math.random() * dailyTasks.length)], + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + startTime: `${h.toString().padStart(2, '0')}:00`, + endTime: `${(h + 1).toString().padStart(2, '0')}:00`, + projectId: pLife.id + } + }); + } + + if (!isWeekend) { + // Work events + let currentHour = 8; + while (currentHour <= 16) { + if (Math.random() > 0.4) { // 60% chance to put a work meeting in this block + const duration = Math.random() > 0.5 ? 1 : 2; + await prisma.task.create({ + data: { + title: workEvents[Math.floor(Math.random() * workEvents.length)], + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + startTime: `${currentHour.toString().padStart(2, '0')}:00`, + endTime: `${(currentHour + duration).toString().padStart(2, '0')}:00`, + projectId: pWork.id + } + }); + currentHour += duration + 1; // leave at least 1h gap + } else { + currentHour += 1; + } + } + } else { + // Weekend Family events + const numFam = Math.floor(Math.random() * 3) + 2; + let famHour = 9; + for (let i = 0; i < numFam; i++) { + await prisma.task.create({ + data: { + title: dailyTasks[Math.floor(Math.random() * dailyTasks.length)], + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + startTime: `${famHour.toString().padStart(2, '0')}:00`, + endTime: `${(famHour + 1).toString().padStart(2, '0')}:00`, + projectId: pFamily.id + } + }); + famHour += 2; + } + } + + // Add April Fools extra + if (isAprilFools) { + await prisma.task.create({ + data: { + title: "Attempt a prank, fail miserably, apologize to HR", + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + startTime: "10:30", + endTime: "11:30", + projectId: pWork.id + } + }); + } + + // Add Birthday extras + if (isBirthday) { + for (let i = 0; i < birthdayTasks.length; i++) { + const time = 14 + i; + await prisma.task.create({ + data: { + title: birthdayTasks[i], + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + startTime: `${time}:00`, + endTime: `${time + 1}:00`, + projectId: pFamily.id + } + }); + } + } + } + + // Populate SomeDay Lists + if (kugelList) { + for(let i=0; i<3; i++) { + await prisma.task.create({ + data: { + title: dailyTasks[Math.floor(Math.random() * dailyTasks.length)], + userId: user.id, + somedayListId: kugelList.id, + order: i + } + }); + } + } + if (papaList) { + for(let i=0; i<3; i++) { + await prisma.task.create({ + data: { + title: workEvents[Math.floor(Math.random() * workEvents.length)], + userId: user.id, + somedayListId: papaList.id, + order: i + } + }); + } + } + + console.log("Densley populated the calendar with wonderful colored events!"); +} + +main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/fill-data.ts b/fill-data.ts new file mode 100644 index 0000000..6ab7416 --- /dev/null +++ b/fill-data.ts @@ -0,0 +1,153 @@ +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); + +const dailyTasks = [ + "Pretend to listen to partner's work drama", + "Feed the kids (again? really?)", + "Hide Amazon packages from spouse", + "Attempt to fold a fitted sheet, give up and roll it into a ball", + "Cook a meal that exactly 0% of the family will appreciate", + "Look at the mess. Sigh. Walk away.", + "Move the laundry from washer to dryer, leave it there for 3 days", + "Find out what died in the fridge", + "Pay bills and cry softly", + "Unload dishwasher with immense resentment", + "Scroll on phone until legs go numb on the toilet", + "Perform archeological dig in the teenage bedroom", + "Interrogate child about missing tupperware lid", + "Nod enthusiastically at toddler's incomprehensible story", + "Try to decipher spouse's 'helpful' grocery list", + "Pretend I don't see the full trash can" +]; + +const workEvents = [ + "Meaningless sync meeting #42", + "Stare blankly at spreadsheet", + "Listen to boss talk about synergy", + "Reply 'As per my previous email...' to Gary", + "Pretend to be busy so no one asks me for help", + "'Quick chat' that ruins my entire afternoon", + "Update Jira tickets to make it look like I did something", + "Mute mic and eat aggressively loud chips during all-hands", + "Consider moving to the woods and becoming a hermit", + "Frantically search for the tab that is playing music" +]; + +const birthdayTasks = [ + "Buy a gift that makes me look thoughtful but was actually on sale", + "Wrap the present (using newspaper because I forgot wrapping paper)", + "Attend birthday party and strategically position myself near the snack table", + "Fake a smile while listening to Uncle Bob's views", + "Smuggle leftover cake home in napkins" +]; + +async function main() { + const user = await prisma.user.findUnique({ + where: { email: 'kugelblitz@gmx.de' } + }); + + if (!user) { + console.log("User kugelblitz@gmx.de not found in database."); + return; + } + console.log(`Using user: ${user.email} (${user.id})`); + + // Target dates: March 16 to April 12, 2026 + const startDate = new Date('2026-03-16T00:00:00Z'); + const endDate = new Date('2026-04-12T00:00:00Z'); + + // Birthday party around April 4th + const birthdayDate = new Date('2026-04-04T00:00:00Z'); + + // April Fools on April 1st + const aprilFools = new Date('2026-04-01T00:00:00Z'); + + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + // ~40% fill rate for normal days + const isBirthday = d.getTime() === birthdayDate.getTime(); + const isAprilFools = d.getTime() === aprilFools.getTime(); + + // Skip ~60% of the regular days to keep density 30-50% + if (!isBirthday && !isAprilFools && Math.random() > 0.45) { + continue; + } + + const dateStr = d.toISOString().split('T')[0]; + const isWeekend = d.getDay() === 0 || d.getDay() === 6; + + // Add 1-2 daily tasks (without specific time) + const numTasks = Math.floor(Math.random() * 2) + 1; + for (let i = 0; i < numTasks; i++) { + const taskName = dailyTasks[Math.floor(Math.random() * dailyTasks.length)]; + await prisma.task.create({ + data: { + title: taskName, + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + order: i, + } + }); + } + + // Add 1-2 work events (with time) if it's a weekday + if (!isWeekend) { + if (Math.random() > 0.3) { + const numEvents = Math.floor(Math.random() * 2) + 1; + let currentHour = Math.floor(Math.random() * (11 - 8 + 1)) + 8; // Morning 8-11 + + for (let e = 0; e < numEvents; e++) { + const eventName = workEvents[Math.floor(Math.random() * workEvents.length)]; + const startTime = `${currentHour.toString().padStart(2, '0')}:00`; + await prisma.task.create({ + data: { + title: eventName, + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + startTime: startTime, + endTime: `${(currentHour+1).toString().padStart(2, '0')}:00`, + } + }); + currentHour += Math.floor(Math.random() * 3) + 2; // Jump 2-4 hours for next event + if (currentHour > 17) break; + } + } + } + + // Add April Fools extra + if (isAprilFools) { + await prisma.task.create({ + data: { + title: "Attempt a prank, fail miserably, apologize to HR", + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + startTime: "10:30", + endTime: "11:00", + } + }); + } + + // Add Birthday extras + if (isBirthday) { + for (let i = 0; i < birthdayTasks.length; i++) { + const time = i >= 2 ? `${14 + i}:00` : undefined; // Party elements have times 16:00, 17:00, 18:00 + await prisma.task.create({ + data: { + title: birthdayTasks[i], + userId: user.id, + scheduledDate: new Date(dateStr), + dayOfWeek: d.getDay(), + startTime: time, + endTime: time ? `${15 + i}:00` : undefined, + } + }); + } + } + } + + console.log("Successfully seeded the calendar with fun, sarcastic data!"); +} + +main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/fill-ext-events.js b/fill-ext-events.js new file mode 100644 index 0000000..ab0d832 --- /dev/null +++ b/fill-ext-events.js @@ -0,0 +1,118 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +const eventNames = [ + "Dentist Appointment", + "Coffee with Sarah", + "Quarterly Review", + "Car Inspection", + "Therapy Session", + "Lunch with Bob" +]; + +const allDayEvents = [ + "Spring Festival", + "Bank Holiday", + "Company Offsite", + "Project Deadline" +]; + +function getWeekStart(date) { + const d = new Date(date); + d.setUTCHours(0, 0, 0, 0); + const day = d.getUTCDay(); + const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1); // Monday is 1 + d.setUTCDate(diff); + return d; +} + +async function main() { + const user = await prisma.user.findUnique({ + where: { email: 'kugelblitz@gmx.de' } + }); + + if (!user) return; + + // 1. Ensure a dummy calendar connection exists + let connection = await prisma.calendarConnection.findFirst({ + where: { userId: user.id, provider: 'google' } + }); + if (!connection) { + connection = await prisma.calendarConnection.create({ + data: { + userId: user.id, + provider: 'google', + accessToken: 'dummy-token', + calendars: JSON.stringify([{ id: 'primary', name: 'My Calendar', color: '#4285F4' }]) + } + }); + } + + // 2. Clear old generated mock events + const mockTitles = [...eventNames, ...allDayEvents]; + await prisma.cachedCalendarEvent.deleteMany({ + where: { + connectionId: connection.id, + title: { in: mockTitles } + } + }); + + const startDate = new Date('2026-03-16T00:00:00Z'); + const endDate = new Date('2026-04-12T00:00:00Z'); + + let idCounter = 1; + + for (let d = new Date(startDate); d <= endDate; d.setUTCDate(d.getUTCDate() + 1)) { + console.log('Processing date:', d.toISOString()); + const dateStr = d.toISOString().split('T')[0]; + const weekStart = getWeekStart(d); + + if (Math.random() > 0.6) { + // Add All-Day Event + await prisma.cachedCalendarEvent.create({ + data: { + userId: user.id, + externalId: `mock-allday-${idCounter++}`, + connectionId: connection.id, + provider: 'google', + calendarId: 'primary', + calendarTitle: 'My Calendar', + calendarColor: '#EA4335', + title: allDayEvents[Math.floor(Math.random() * allDayEvents.length)], + startDate: dateStr, + endDate: dateStr, + weekStart: weekStart + } + }); + } + + if (Math.random() > 0.5) { + // Add Timed Event + const hour = Math.floor(Math.random() * 6) + 10; // 10 to 15 + const startDt = new Date(d); + startDt.setUTCHours(hour, 0, 0, 0); + const endDt = new Date(d); + endDt.setUTCHours(hour + 1, 0, 0, 0); + + await prisma.cachedCalendarEvent.create({ + data: { + userId: user.id, + externalId: `mock-timed-${idCounter++}`, + connectionId: connection.id, + provider: 'google', + calendarId: 'primary', + calendarTitle: 'My Calendar', + calendarColor: '#4285F4', + title: eventNames[Math.floor(Math.random() * eventNames.length)], + startDateTime: startDt, + endDateTime: endDt, + weekStart: weekStart + } + }); + } + } + + console.log("Successfully added simulated external calendar events!"); +} + +main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/fill-ext-events.ts b/fill-ext-events.ts new file mode 100644 index 0000000..2f09dc2 --- /dev/null +++ b/fill-ext-events.ts @@ -0,0 +1,116 @@ +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); + +const eventNames = [ + "Dentist Appointment", + "Coffee with Sarah", + "Quarterly Review", + "Car Inspection", + "Therapy Session", + "Lunch with Bob" +]; + +const allDayEvents = [ + "Spring Festival", + "Bank Holiday", + "Company Offsite", + "Project Deadline" +]; + +function getWeekStart(date: Date): Date { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + const day = d.getDay(); + const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday is 1 + return new Date(d.setDate(diff)); +} + +async function main() { + const user = await prisma.user.findUnique({ + where: { email: 'kugelblitz@gmx.de' } + }); + + if (!user) return; + + // 1. Ensure a dummy calendar connection exists + let connection = await prisma.calendarConnection.findFirst({ + where: { userId: user.id, provider: 'google' } + }); + if (!connection) { + connection = await prisma.calendarConnection.create({ + data: { + userId: user.id, + provider: 'google', + accessToken: 'dummy-token', + calendars: JSON.stringify([{ id: 'primary', name: 'My Calendar', color: '#4285F4' }]) + } + }); + } + + // 2. Clear old generated mock events + const mockTitles = [...eventNames, ...allDayEvents]; + await prisma.cachedCalendarEvent.deleteMany({ + where: { + connectionId: connection.id, + title: { in: mockTitles } + } + }); + + const startDate = new Date('2026-03-16T00:00:00Z'); + const endDate = new Date('2026-04-12T00:00:00Z'); + + let idCounter = 1; + + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const dateStr = d.toISOString().split('T')[0]; + const weekStart = getWeekStart(d); + + if (Math.random() > 0.6) { + // Add All-Day Event + await prisma.cachedCalendarEvent.create({ + data: { + userId: user.id, + externalId: `mock-allday-${idCounter++}`, + connectionId: connection.id, + provider: 'google', + calendarId: 'primary', + calendarTitle: 'My Calendar', + calendarColor: '#primary', // Or any string, frontend handles # + title: allDayEvents[Math.floor(Math.random() * allDayEvents.length)], + startDate: dateStr, + endDate: dateStr, + weekStart: weekStart + } + }); + } + + if (Math.random() > 0.5) { + // Add Timed Event + const hour = Math.floor(Math.random() * 6) + 10; // 10 to 15 + const startDt = new Date(d); + startDt.setUTCHours(hour, 0, 0, 0); + const endDt = new Date(d); + endDt.setUTCHours(hour + 1, 0, 0, 0); + + await prisma.cachedCalendarEvent.create({ + data: { + userId: user.id, + externalId: `mock-timed-${idCounter++}`, + connectionId: connection.id, + provider: 'google', + calendarId: 'primary', + calendarTitle: 'My Calendar', + calendarColor: '#EA4335', + title: eventNames[Math.floor(Math.random() * eventNames.length)], + startDateTime: startDt, + endDateTime: endDt, + weekStart: weekStart + } + }); + } + } + + console.log("Successfully added simulated external calendar events!"); +} + +main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/src/app/api/auth/signup/route.ts b/src/app/api/auth/signup/route.ts index 1b4f560..c1c4ccd 100644 --- a/src/app/api/auth/signup/route.ts +++ b/src/app/api/auth/signup/route.ts @@ -1,12 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; -import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; import { hash } from 'bcryptjs'; import { sendVerificationEmail, generateVerificationCode, generateVerificationToken } from '@/lib/email'; import { languageFromAcceptLanguage } from '@/lib/emailTemplates'; -const prisma = new PrismaClient(); - export async function POST(request: NextRequest) { try { const body = await request.json(); diff --git a/src/app/api/calendar/apple/connect/route.ts b/src/app/api/calendar/apple/connect/route.ts index 57f4845..0486d89 100644 --- a/src/app/api/calendar/apple/connect/route.ts +++ b/src/app/api/calendar/apple/connect/route.ts @@ -3,9 +3,7 @@ import { validateCredentials } from '@/lib/apple-calendar'; import { CalendarConnection } from '@/lib/calendar-sync'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); +import { prisma } from '@/lib/prisma'; export async function POST(req: Request) { try { diff --git a/src/app/api/calendar/google/oauth/route.ts b/src/app/api/calendar/google/oauth/route.ts index ac45006..024b6ca 100644 --- a/src/app/api/calendar/google/oauth/route.ts +++ b/src/app/api/calendar/google/oauth/route.ts @@ -2,12 +2,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { google } from 'googleapis'; -import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; export const dynamic = 'force-dynamic'; -const prisma = new PrismaClient(); - // Google Calendar OAuth callback endpoint export async function GET(request: NextRequest) { try { diff --git a/src/app/api/calendar/notion/callback/route.ts b/src/app/api/calendar/notion/callback/route.ts index ba049a9..e5d1422 100644 --- a/src/app/api/calendar/notion/callback/route.ts +++ b/src/app/api/calendar/notion/callback/route.ts @@ -1,13 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; import { getUserDatabases } from '@/lib/notion-calendar'; export const dynamic = 'force-dynamic'; -const prisma = new PrismaClient(); - export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions); diff --git a/src/app/api/calendar/synology/connect/route.ts b/src/app/api/calendar/synology/connect/route.ts index b70755b..986a565 100644 --- a/src/app/api/calendar/synology/connect/route.ts +++ b/src/app/api/calendar/synology/connect/route.ts @@ -2,9 +2,7 @@ import { NextResponse } from 'next/server'; import { validateCredentials } from '@/lib/synology-calendar'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); +import { prisma } from '@/lib/prisma'; export async function POST(req: Request) { try { diff --git a/src/app/api/goal/route.ts b/src/app/api/goal/route.ts index ac3ab95..106062d 100644 --- a/src/app/api/goal/route.ts +++ b/src/app/api/goal/route.ts @@ -127,7 +127,16 @@ export async function GET(req: Request) { }; // Try each user-configured URL first - const userUrls: string[] = (user as any)?.quoteSourceUrls?.filter(Boolean) || []; + const PRIVATE_IP = /^(localhost|127\.|0\.0\.0\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|::1$|fc00:|fe80:)/i; + function isSafeQuoteUrl(raw: string): boolean { + try { + const u = new URL(raw); + if (!['http:', 'https:'].includes(u.protocol)) return false; + if (PRIVATE_IP.test(u.hostname)) return false; + return true; + } catch { return false; } + } + const userUrls: string[] = ((user as any)?.quoteSourceUrls?.filter(Boolean) || []).filter(isSafeQuoteUrl); for (const url of userUrls) { try { const res = await fetch(url, { signal: AbortSignal.timeout(4000) }); diff --git a/src/app/api/someday-lists/external/route.ts b/src/app/api/someday-lists/external/route.ts index f7a9abf..469e3de 100644 --- a/src/app/api/someday-lists/external/route.ts +++ b/src/app/api/someday-lists/external/route.ts @@ -2,15 +2,13 @@ import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; import { createGoogleClient, createGoogleTaskList } from '@/lib/google-tasks'; import { createMsTodoList } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; import { createAppleReminderList } from '@/lib/apple-reminders'; import { createSynologyReminderList } from '@/lib/synology-tasks'; -const prisma = new PrismaClient(); - export async function POST(req: NextRequest) { try { const session = await getServerSession(authOptions); diff --git a/src/app/api/someday-lists/route.ts b/src/app/api/someday-lists/route.ts index 8bd6ce6..8f4e191 100644 --- a/src/app/api/someday-lists/route.ts +++ b/src/app/api/someday-lists/route.ts @@ -1,11 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; import { notifyUser } from '@/lib/sse'; -const prisma = new PrismaClient(); - async function resolveUserId(email: string): Promise { const user = await prisma.user.findUnique({ where: { email }, diff --git a/src/app/api/tasks/import/route.ts b/src/app/api/tasks/import/route.ts index 9dc30b0..28a6916 100644 --- a/src/app/api/tasks/import/route.ts +++ b/src/app/api/tasks/import/route.ts @@ -2,13 +2,11 @@ import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; import { createGoogleClient, fetchGoogleTasks, fetchGoogleTaskLists } from '@/lib/google-tasks'; import { fetchMsTodoLists, fetchMsTodoTasks, isMsTodoTaskCompleted, fetchMsChecklistItems } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; -const prisma = new PrismaClient(); - interface SourceList { id: string; title: string; diff --git a/src/app/api/tasks/lists/route.ts b/src/app/api/tasks/lists/route.ts index 0284f21..43eb457 100644 --- a/src/app/api/tasks/lists/route.ts +++ b/src/app/api/tasks/lists/route.ts @@ -1,15 +1,13 @@ import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; import { createGoogleClient, fetchGoogleTaskLists } from '@/lib/google-tasks'; import { fetchMsTodoLists } from '@/lib/microsoft-todo'; import { getOutlookAccessToken } from '@/lib/outlook-token'; export const dynamic = 'force-dynamic'; -const prisma = new PrismaClient(); - export async function GET(req: NextRequest) { try { const session = await getServerSession(authOptions); diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 8c3b647..8d18ca7 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -1,11 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; -import { PrismaClient, Task } from '@prisma/client'; +import { Task } from '@prisma/client'; +import { prisma } from '@/lib/prisma'; import { notifyUser } from '@/lib/sse'; -const prisma = new PrismaClient(); - // Validate and sanitize a URL — only allow http/https, reject javascript: and data: schemes function sanitizeUrl(raw: string | null | undefined): string | null { if (!raw) return null; diff --git a/src/app/api/user/export-week-view/route.ts b/src/app/api/user/export-week-view/route.ts index 45107f5..b0fa81d 100644 --- a/src/app/api/user/export-week-view/route.ts +++ b/src/app/api/user/export-week-view/route.ts @@ -114,6 +114,22 @@ function dayDateLabel(d: Date, de: boolean): string { }); } +// Strip Apple Calendar icon encodings (=h=g, =i=g, etc.) and emoji from +// event titles before PDF rendering. Helvetica cannot render these glyphs and +// they appear as raw escape sequences. The colored dot/chip already identifies +// the calendar visually, so the icon character carries no extra information. +function pdfTitle(s: string | null | undefined, maxLen = 60): string { + if (!s) return ''; + return s + .replace(/(?:=[a-zA-Z])+/g, '') // Apple CalDAV icon escapes: =h=g, =i=g, =k=g … + .replace(/[\u{1F000}-\u{1FFFF}]/gu, '') // emoji (supplementary plane) + .replace(/[\u{2600}-\u{27BF}]/gu, '') // misc symbols & dingbats + .replace(/[\uE000-\uF8FF]/g, '') // Apple private-use area + .replace(/[\uFE00-\uFE0F\u200D]/g, '') // variation selectors / ZWJ + .trim() + .slice(0, maxLen); +} + function fmtHour(h: number): string { return `${String(h).padStart(2, '0')}:00`; } @@ -454,7 +470,7 @@ function WeekCalendarPDF({ return React.createElement(View, { key: t.id, style: { borderRadius: 2, paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2, backgroundColor: DONE_BG }, - }, React.createElement(Text, { style: { fontSize: 7.5, color: DONE_TEXT } }, (t.title || '').slice(0, 36))); + }, React.createElement(Text, { style: { fontSize: 7.5, color: DONE_TEXT } }, pdfTitle(t.title, 36))); } if (t.isExternal) { // Calendar event: solid color chip with white text + dot indicator (matches webapp style) @@ -473,7 +489,7 @@ function WeekCalendarPDF({ React.createElement(View, { style: { width: 5, height: 5, borderRadius: 2.5, backgroundColor: evTextColor, opacity: 0.7, marginRight: 3, flexShrink: 0 }, }), - React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: evTextColor, flex: 1 } }, (t.title || '').slice(0, 36)), + React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: evTextColor, flex: 1 } }, pdfTitle(t.title, 36)), ); } // User-created all-day task: left border only, no background fill @@ -481,7 +497,7 @@ function WeekCalendarPDF({ return React.createElement(View, { key: t.id, style: { borderLeftWidth: 2, borderLeftColor: accent, paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2 }, - }, React.createElement(Text, { style: { fontSize: 7.5, color: '#374151' } }, (t.title || '').slice(0, 36))); + }, React.createElement(Text, { style: { fontSize: 7.5, color: '#374151' } }, pdfTitle(t.title, 36))); }), allDayTasks.length > 5 ? React.createElement(Text, { style: { fontSize: 6, color: '#3b82f6', marginTop: 1 }, @@ -575,7 +591,7 @@ function WeekCalendarPDF({ key: t.id, style: { marginBottom: 1, paddingLeft: 1, paddingVertical: 1 }, }, - React.createElement(Text, { style: { fontSize: 7.5, color: DONE_TEXT, lineHeight: 1.3, textDecoration: 'line-through' } }, (t.title || '').slice(0, 60)), + React.createElement(Text, { style: { fontSize: 7.5, color: DONE_TEXT, lineHeight: 1.3, textDecoration: 'line-through' } }, pdfTitle(t.title)), ); } @@ -589,7 +605,7 @@ function WeekCalendarPDF({ paddingLeft: 3, paddingVertical: 1, marginBottom: 1, }, }, - React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: '#1e293b', lineHeight: 1.3 } }, (t.title || '').slice(0, 60)), + React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: '#1e293b', lineHeight: 1.3 } }, pdfTitle(t.title)), ); } @@ -601,7 +617,7 @@ function WeekCalendarPDF({ paddingVertical: 1, marginBottom: 1, }, }, - React.createElement(Text, { style: { fontSize: 7.5, color: '#1e293b', lineHeight: 1.3 } }, (t.title || '').slice(0, 60)), + React.createElement(Text, { style: { fontSize: 7.5, color: '#1e293b', lineHeight: 1.3 } }, pdfTitle(t.title)), ); }), ); @@ -719,7 +735,7 @@ function WeekCalendarPDF({ }), React.createElement(Text, { style: { fontSize: 7, color: '#374151', lineHeight: 1.3, flex: 1 }, - }, (t.title || '').slice(0, 52)), + }, pdfTitle(t.title)), ) ), list.tasks.length > 18 ? React.createElement(Text, { diff --git a/src/components/GridTaskBlock.tsx b/src/components/GridTaskBlock.tsx index 4799cb0..451fbc1 100644 --- a/src/components/GridTaskBlock.tsx +++ b/src/components/GridTaskBlock.tsx @@ -488,6 +488,25 @@ export function GridTaskBlock({ )} + {/* Rolling indicator */} + {task.isRolling && !task.completed && ( + + + + )} {weatherEnabled && (() => { const provider = task.externalProvider || (task.externalId?.startsWith("synology::") ? "synology" : null); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 8841417..0b15f86 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -13,7 +13,7 @@ export const authOptions: NextAuthOptions = { GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID || "", clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", - allowDangerousEmailAccountLinking: true, + allowDangerousEmailAccountLinking: false, authorization: { params: { prompt: "consent", @@ -26,13 +26,13 @@ export const authOptions: NextAuthOptions = { AppleProvider({ clientId: process.env.APPLE_ID || "", clientSecret: process.env.APPLE_SECRET || "", - allowDangerousEmailAccountLinking: true, + allowDangerousEmailAccountLinking: false, }), AzureADProvider({ clientId: process.env.MICROSOFT_CLIENT_ID || "", clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "", tenantId: "common", - allowDangerousEmailAccountLinking: true, + allowDangerousEmailAccountLinking: false, authorization: { params: { prompt: "consent", @@ -85,7 +85,7 @@ export const authOptions: NextAuthOptions = { } }) ], - debug: true, + debug: process.env.NODE_ENV === 'development', session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60, // 30 days diff --git a/src/lib/email.ts b/src/lib/email.ts index 438992f..de9e995 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -88,9 +88,11 @@ export async function sendVerificationEmail( const from = process.env.SMTP_FROM || '"My Weekly ToDo\'s" '; console.log(`[EMAIL] Sending verification email to ${email} from ${from}`); - console.log(`\n======================================================`); - console.log(`[DEV VERIFICATION LINK]:\n${verifyLink}`); - console.log(`======================================================\n`); + if (process.env.NODE_ENV === 'development') { + console.log(`\n======================================================`); + console.log(`[DEV VERIFICATION LINK]:\n${verifyLink}`); + console.log(`======================================================\n`); + } try { const timeoutPromise = new Promise((_, reject) => @@ -159,9 +161,11 @@ export async function sendPasswordResetEmail(email: string, token: string, langu const from = process.env.SMTP_FROM || '"My Weekly ToDo\'s" '; console.log(`[EMAIL] Sending password reset email to ${email} from ${from}`); - console.log(`\n======================================================`); - console.log(`[DEV RESET LINK]:\n${resetLink}`); - console.log(`======================================================\n`); + if (process.env.NODE_ENV === 'development') { + console.log(`\n======================================================`); + console.log(`[DEV RESET LINK]:\n${resetLink}`); + console.log(`======================================================\n`); + } try { const timeoutPromise = new Promise((_, reject) =>