Feature/meeting attendance tracking#10
Conversation
- Add AttendanceSource enum (SLACK_HUDDLE, MESSAGE_ACTIVITY, MENTOR_REPORT, MANUAL) - Extend MeetingAttendance model with source, confidence, and metadata fields - Extend Meeting model with Slack integration fields (slackHuddleId, scheduledStartAt, scheduledEndAt) - Add projectId relation to Meeting model - Add meetings relation to Project model - Add indexes for performance (source, projectId) - Create migration file for database updates
- Add Meeting, MeetingAttendance, and MeetingResponse GraphQL types - Register AttendanceSource enum in GraphQL schema - Create input types: MeetingCreateInput, MeetingAttendanceInput - Add MeetingResolver with queries and mutations: - meetings: List meetings by event/project - meeting: Get single meeting by ID - createMeeting: Create new meeting - recordMeetingAttendance: Record/update attendance (upsert logic) - meetingAttendance: Get all attendance for a meeting - Auto-loaded resolver via existing dynamic loading - All types use Prisma.JsonValue for JSON fields
…ance - Create processMentorReflections automation task - Runs every 6 hours to check for new mentor survey responses - Extracts attendance data from reflection responses: - response.meetingHeld: whether meeting occurred - response.studentAttendance: array of attending student IDs - response.studentsPresent: alternative format - Automatically creates Meeting records for the week if needed - Records MeetingAttendance with source=MENTOR_REPORT - Supports multiple response formats for flexibility - Upsert logic: updates existing records or creates new ones - Stores reflection metadata for audit trail
Dashboard Queries: - statStudentAttendance: Get attendance stats for all students - Calculates attendance %, meetings attended/total - Flags students below threshold (default 75%) - Tracks data sources (MENTOR_REPORT, SLACK_HUDDLE, etc.) - Supports filtering by event/project - statMentorReflectionCompletion: Track mentor reflection submissions - Calculates expected vs submitted reflections - Flags mentors below 75% completion - flaggedStudents: Get list of students needing attention - Combines attendance data with mentor info - Provides actionable insights (reason, last attended, etc.) Alerting System: - sendAttendanceAlerts automation task (runs weekly on Mondays) - Checks all active events for attendance issues - Identifies students with <75% attendance (min 2 meetings) - Identifies mentors behind on reflections - Sends Slack alerts to mentor channel with summary - Email template for weekly attendance reports New Types: - StudentAttendanceStat: Detailed attendance metrics per student - MentorReflectionStat: Reflection completion metrics per mentor - FlaggedStudent: Combined student + issue information
… message in stats channel.
| for (const stat of attendanceStats.filter((s) => s.isFlagged)) { | ||
| const mentor = stat.project?.mentors?.[0]; |
There was a problem hiding this comment.
flaggedStudents always returns undefined mentor
stat.project?.mentors?.[0] will always be undefined because the underlying call to statStudentAttendance does not include the mentors relation on projects — only meetings and its attendance are included. Every FlaggedStudent will therefore have mentor: undefined, making the mentor field in the response always null regardless of the project's actual mentors.
| scheduledStartAt: weekStart, | ||
| scheduledEndAt: weekStart, |
There was a problem hiding this comment.
scheduledEndAt is set to weekStart instead of weekEnd, so every auto-created meeting will have a zero-duration window (start equals end). This will make the meeting appear to end the moment it begins in any UI that surfaces scheduled times.
| scheduledStartAt: weekStart, | |
| scheduledEndAt: weekStart, | |
| scheduledStartAt: weekStart, | |
| scheduledEndAt: weekEnd, |
| @Authorized(AuthRole.ADMIN, AuthRole.MANAGER, AuthRole.MENTOR) | ||
| @Mutation(() => MeetingAttendance) | ||
| async recordMeetingAttendance( | ||
| @Ctx() { auth }: Context, | ||
| @Arg('data', () => MeetingAttendanceInput) data: MeetingAttendanceInput, | ||
| ): Promise<PrismaMeetingAttendance> { | ||
| DEBUG(`Recording attendance for meeting ${data.meetingId}, student ${data.studentId}: ${data.attended}`); | ||
|
|
||
| // Check for existing attendance record | ||
| const existing = await this.prisma.meetingAttendance.findFirst({ | ||
| where: { | ||
| meetingId: data.meetingId, | ||
| studentId: data.studentId, | ||
| }, | ||
| }); | ||
|
|
||
| if (existing) { | ||
| // Update existing record | ||
| return this.prisma.meetingAttendance.update({ | ||
| where: { id: existing.id }, | ||
| data: { | ||
| attended: data.attended, | ||
| prepared: data.prepared ?? existing.prepared, | ||
| source: data.source ?? existing.source, | ||
| confidence: data.confidence ?? existing.confidence, | ||
| metadata: data.metadata as any ?? existing.metadata, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| // Create new record | ||
| return this.prisma.meetingAttendance.create({ | ||
| data: { | ||
| meetingId: data.meetingId, | ||
| studentId: data.studentId, | ||
| attended: data.attended, | ||
| prepared: data.prepared ?? false, | ||
| source: data.source ?? 'MANUAL', | ||
| confidence: data.confidence ?? 1.0, | ||
| metadata: data.metadata as any, | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Mentor can record attendance for any meeting
The recordMeetingAttendance mutation is accessible to the MENTOR role but performs no check that the targeted meetingId belongs to a project the authenticated mentor is actually assigned to. Any mentor with a valid token can overwrite attendance records for meetings in other mentors' projects or even other events, allowing incorrect data to be written at will.
| @Authorized(AuthRole.ADMIN, AuthRole.MANAGER, AuthRole.MENTOR) | ||
| @Query(() => [MeetingAttendance]) | ||
| async meetingAttendance( | ||
| @Ctx() { auth }: Context, | ||
| @Arg('meetingId', () => String) meetingId: string, | ||
| ): Promise<PrismaMeetingAttendance[]> { | ||
| return this.prisma.meetingAttendance.findMany({ | ||
| where: { meetingId }, | ||
| include: { student: true }, | ||
| orderBy: { createdAt: 'asc' }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Mentor can read attendance for any meeting
The meetingAttendance query accepts any meetingId and returns all attendance records for it without verifying that the requesting mentor is associated with that meeting's project. A mentor can enumerate attendance data for every other team in the event by guessing or iterating meeting IDs.
| @Authorized(AuthRole.ADMIN, AuthRole.MANAGER, AuthRole.MENTOR, AuthRole.STUDENT) | ||
| @Query(() => Meeting, { nullable: true }) | ||
| async meeting( | ||
| @Ctx() { auth }: Context, | ||
| @Arg('id', () => String) id: string, | ||
| ): Promise<PrismaMeeting | null> { | ||
| const meeting = await this.prisma.meeting.findUnique({ | ||
| where: { id }, | ||
| include: { project: true }, | ||
| }); | ||
|
|
||
| if (!meeting) return null; | ||
|
|
||
| // Verify access | ||
| if (!auth.isAdmin && !auth.isManager) { | ||
| if (auth.isMentor || auth.isStudent) { | ||
| if (meeting.eventId !== auth.eventId) { | ||
| throw new Error('No permission to view this meeting.'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return meeting; |
There was a problem hiding this comment.
Overly broad event-level access for mentor/student on
meeting query
The access check for mentors and students only verifies that meeting.eventId === auth.eventId, but a meeting may belong to a different project within the same event. A mentor or student can therefore retrieve meeting details (including schedule and attendance) for any project in their event, not just their own team.
| // Check mentor reflection completion | ||
| const mentorReflections = await prisma.surveyResponse.count({ | ||
| where: { | ||
| authorMentorId: mentor.id, | ||
| surveyOccurence: { | ||
| survey: { | ||
| personType: 'MENTOR', | ||
| eventId: event.id, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
There was a problem hiding this comment.
prisma.surveyResponse.count() is called inside the for (const project of projects) loop, issuing one separate database round-trip per mentor. For an event with 100 mentors this becomes 100 sequential queries. The same pattern exists in scripts/testAttendanceSlack.ts. Consider grouping these counts with a single groupBy call outside the loop.
| --- | ||
| to: "akif@codeday.org" | ||
| subject: "Weekly Attendance Report for {{ event.name }}" | ||
| --- |
There was a problem hiding this comment.
Hardcoded recipient email address
The template's to: frontmatter is set to a hardcoded personal email address (akif@codeday.org). If this template is ever wired up to the email-sending pipeline it will route all weekly attendance reports directly to that inbox regardless of the event's configured contact. The address should be a template variable like {{ event.contactEmail }} or removed if the template is not yet used.
🤖 Augment PR SummarySummary: Adds meeting attendance tracking to Labs GraphQL, primarily sourced from mentor reflection reporting and used to drive weekly alerting. Changes:
🤖 Was this summary useful? React with 👍 or 👎 |
| import { DateTime } from 'luxon'; | ||
|
|
||
| const DEBUG = makeDebug('automation:tasks:sendAttendanceAlerts'); | ||
| const ATTENDANCE_ALERT_CHANNEL = 'stats'; |
There was a problem hiding this comment.
src/automation/tasks/sendAttendanceAlerts.ts:8 ATTENDANCE_ALERT_CHANNEL is set to 'stats', but Slack chat.postMessage expects a channel ID (and other Slack flows here persist IDs like slackMentorChannelId), so this may fail to post. Consider resolving #stats to an ID or using an event-configured channel ID.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| const flagged: FlaggedStudent[] = []; | ||
|
|
||
| for (const stat of attendanceStats.filter((s) => s.isFlagged)) { | ||
| const mentor = stat.project?.mentors?.[0]; |
There was a problem hiding this comment.
src/resolvers/Stats.ts:276 stat.project?.mentors?.[0] will always be undefined here because statStudentAttendance doesn’t include mentors on the loaded project, so flaggedStudents likely returns mentor: null even when a mentor exists. Consider including mentors when building the stats (or fetching the mentor separately) before populating FlaggedStudent.mentor.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| DEBUG(`Recording attendance for meeting ${data.meetingId}, student ${data.studentId}: ${data.attended}`); | ||
|
|
||
| // Check for existing attendance record | ||
| const existing = await this.prisma.meetingAttendance.findFirst({ |
There was a problem hiding this comment.
src/resolvers/Meeting.ts:98 recordMeetingAttendance updates/creates by meetingId+studentId without validating that the meeting/student belong to auth.eventId (and for mentors, that they’re associated with the meeting’s project), which could allow cross-project/event edits if IDs are known. The same scoping concern also applies to meetingAttendance (src/resolvers/Meeting.ts:139).
Severity: high
Other Locations
src/resolvers/Meeting.ts:139
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| let meeting = await prisma.meeting.findFirst({ | ||
| where: { | ||
| projectId: project.id, | ||
| scheduledStartAt: { gte: weekStart, lte: weekEnd }, |
There was a problem hiding this comment.
src/automation/tasks/processMentorReflections.ts:134 The meeting lookup keys off scheduledStartAt, but this column is newly added and may be NULL for existing Meeting rows, which can cause duplicate meetings to be created for the same project/week. Consider matching on the existing visibleAt/dueAt week window or explicitly handling scheduledStartAt being null.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| visibleAt: weekStart, | ||
| dueAt: weekEnd, | ||
| scheduledStartAt: weekStart, | ||
| scheduledEndAt: weekStart, |
There was a problem hiding this comment.
src/automation/tasks/processMentorReflections.ts:148 When auto-creating a weekly meeting, scheduledEndAt is set to weekStart, making start/end identical and potentially confusing downstream ordering/reporting. Consider setting a more representative end timestamp (e.g., weekEnd or a default duration).
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
Adds ability to store data regarding whether students are attending meetings with their team through mentor reporting. Adds automation for slack notification of which students aren't attending meetings.