diff --git a/.changeset/event-visibility-fix.md b/.changeset/event-visibility-fix.md new file mode 100644 index 0000000000..17c91e4e55 --- /dev/null +++ b/.changeset/event-visibility-fix.md @@ -0,0 +1,4 @@ +--- +--- + +Internal: Fix event visibility and registration matching (no protocol changes) diff --git a/server/src/addie/mcp/event-tools.ts b/server/src/addie/mcp/event-tools.ts index 3d8d726d4a..6b20943e82 100644 --- a/server/src/addie/mcp/event-tools.ts +++ b/server/src/addie/mcp/event-tools.ts @@ -171,8 +171,10 @@ Optional: description, end_time, timezone, location details, virtual_url, max_at }, }, { - name: 'list_upcoming_events', - description: `List upcoming AAO events. Use this when someone asks about upcoming events, the events calendar, or what's happening soon.`, + name: 'list_events', + description: `List AAO events. Use this when someone asks about events - either upcoming or past events. +When asked about past events, historical events, or what events have happened, set include_past=true. +When asked about upcoming events or what's happening soon, use the defaults (include_past=false).`, input_schema: { type: 'object' as const, properties: { @@ -181,6 +183,10 @@ Optional: description, end_time, timezone, location details, virtual_url, max_at enum: ['summit', 'meetup', 'webinar', 'workshop', 'conference', 'other'], description: 'Filter by event type', }, + include_past: { + type: 'boolean', + description: 'Include past/completed events (default: false, only shows upcoming)', + }, limit: { type: 'number', description: 'Maximum number of events to return (default: 5)', @@ -435,27 +441,39 @@ export function createEventToolHandlers( return response; }); - // List upcoming events - handlers.set('list_upcoming_events', async (input) => { + // List events (upcoming or past) + handlers.set('list_events', async (input) => { const eventType = input.event_type as EventType | undefined; + const includePast = input.include_past === true; const limit = Math.min((input.limit as number) || 5, 20); - const events = await eventsDb.listEvents({ - status: 'published', + // Query events based on whether we want past or upcoming + const statuses = includePast + ? ['published', 'completed'] as const + : ['published'] as const; + + let allEvents = await eventsDb.listEvents({ + statuses: [...statuses], event_type: eventType, - upcoming_only: true, + past_only: includePast, + upcoming_only: !includePast, limit, }); - if (events.length === 0) { - let msg = 'No upcoming events found'; + // Sort descending for past events (most recent first) + if (includePast) { + allEvents.sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime()); + } + + if (allEvents.length === 0) { + let msg = includePast ? 'No past events found' : 'No upcoming events found'; if (eventType) msg += ` of type "${eventType}"`; return msg + '.'; } - let response = `## Upcoming Events\n\n`; + let response = includePast ? `## Past Events\n\n` : `## Upcoming Events\n\n`; - for (const event of events) { + for (const event of allEvents) { const typeEmoji = { summit: '🏔️', meetup: '🤝', @@ -476,7 +494,7 @@ export function createEventToolHandlers( response += ` 💻 Virtual\n`; } - if (event.luma_url) { + if (!includePast && event.luma_url) { response += ` 🔗 Register: ${event.luma_url}\n`; } response += `\n`; diff --git a/server/src/db/events-db.ts b/server/src/db/events-db.ts index c0f90432ed..d0514b671f 100644 --- a/server/src/db/events-db.ts +++ b/server/src/db/events-db.ts @@ -202,7 +202,12 @@ export class EventsDatabase { const params: unknown[] = []; let paramIndex = 1; - if (options.status) { + // Support querying multiple statuses at once + if (options.statuses && options.statuses.length > 0) { + const placeholders = options.statuses.map(() => `$${paramIndex++}`).join(', '); + conditions.push(`status IN (${placeholders})`); + params.push(...options.statuses); + } else if (options.status) { conditions.push(`status = $${paramIndex++}`); params.push(options.status); } @@ -362,9 +367,27 @@ export class EventsDatabase { } /** - * Get registrations by user - */ - async getUserRegistrations(workosUserId: string): Promise { + * Get registrations by user (matches by workos_user_id OR by user's email) + * This allows registrations created via Luma import (which only have email) + * to be associated with a user who later signs up with that email. + * + * Security note: This intentionally associates registrations by email to support + * the workflow where external registrations (Luma) are imported by email, and + * users later sign up with the same email to see their history. + */ + async getUserRegistrations(workosUserId: string, userEmail?: string): Promise { + // If we have an email, also match registrations by email + // This handles Luma imports where workos_user_id wasn't set + if (userEmail) { + const result = await query( + `SELECT * FROM event_registrations + WHERE workos_user_id = $1 OR LOWER(email) = LOWER($2) + ORDER BY registered_at DESC`, + [workosUserId, userEmail] + ); + return result.rows.map(row => this.deserializeRegistration(row)); + } + const result = await query( `SELECT * FROM event_registrations WHERE workos_user_id = $1 @@ -390,9 +413,20 @@ export class EventsDatabase { } /** - * Check if user is registered for an event + * Check if user is registered for an event (by workos_user_id or email) */ - async isUserRegistered(eventId: string, workosUserId: string): Promise { + async isUserRegistered(eventId: string, workosUserId: string, userEmail?: string): Promise { + if (userEmail) { + const result = await query( + `SELECT 1 FROM event_registrations + WHERE event_id = $1 AND (workos_user_id = $2 OR LOWER(email) = LOWER($3)) + AND registration_status != 'cancelled' + LIMIT 1`, + [eventId, workosUserId, userEmail] + ); + return result.rows.length > 0; + } + const result = await query( `SELECT 1 FROM event_registrations WHERE event_id = $1 AND workos_user_id = $2 diff --git a/server/src/routes/events.ts b/server/src/routes/events.ts index 34a7eabb91..3b1489d17b 100644 --- a/server/src/routes/events.ts +++ b/server/src/routes/events.ts @@ -912,7 +912,7 @@ export function createEventsRouter(): { // PUBLIC API ROUTES (mounted at /api/events) // ========================================================================= - // GET /api/events - List published upcoming events + // GET /api/events - List public events (published or completed) publicApiRouter.get("/", async (req: Request, res: Response) => { try { const event_type = req.query.event_type as EventType | undefined; @@ -920,14 +920,26 @@ export function createEventsRouter(): { const upcoming_only = req.query.upcoming !== "false"; // Default to upcoming only const past_only = req.query.past === "true"; + // For past events, include both "published" and "completed" status + // For upcoming events, only show "published" + const statuses: EventStatus[] = past_only + ? ["published", "completed"] + : ["published"]; + const events = await eventsDb.listEvents({ - status: "published", + statuses, event_type, event_format, upcoming_only: past_only ? false : upcoming_only, past_only, }); + // Sort by start_time (ascending for upcoming, descending for past) + // Database returns ASC by default, so only re-sort for past events (DESC) + if (past_only) { + events.sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime()); + } + res.json({ events }); } catch (error) { logger.error({ err: error }, "Error listing public events"); @@ -1012,8 +1024,8 @@ export function createEventsRouter(): { }); } - // Check if already registered - const alreadyRegistered = await eventsDb.isUserRegistered(event.id, user.id); + // Check if already registered (by user ID or email) + const alreadyRegistered = await eventsDb.isUserRegistered(event.id, user.id, user.email); if (alreadyRegistered) { return res.status(400).json({ error: "Already registered", @@ -1072,7 +1084,7 @@ export function createEventsRouter(): { }); } - const registrations = await eventsDb.getUserRegistrations(user.id); + const registrations = await eventsDb.getUserRegistrations(user.id, user.email); const registration = registrations.find((r) => r.event_id === event.id); res.json({ registration: registration || null }); @@ -1102,7 +1114,7 @@ export function createEventsRouter(): { }); } - const registrations = await eventsDb.getUserRegistrations(user.id); + const registrations = await eventsDb.getUserRegistrations(user.id, user.email); const registration = registrations.find((r) => r.event_id === event.id); if (!registration) { diff --git a/server/src/types.ts b/server/src/types.ts index d43287f16a..ce135d93ee 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -660,6 +660,7 @@ export interface UpdateEventInput { export interface ListEventsOptions { status?: EventStatus; + statuses?: EventStatus[]; // Query multiple statuses at once event_type?: EventType; event_format?: EventFormat; upcoming_only?: boolean;