Skip to content

Feature/meeting attendance tracking#10

Open
HanCotterell wants to merge 6 commits into
mainfrom
feature/meeting-attendance-tracking
Open

Feature/meeting attendance tracking#10
HanCotterell wants to merge 6 commits into
mainfrom
feature/meeting-attendance-tracking

Conversation

@HanCotterell

Copy link
Copy Markdown
Contributor

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.

augmentcode Bot and others added 6 commits July 14, 2026 19:34
- 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
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds meeting attendance tracking to the Labs GraphQL API — storing attendance records sourced from mentor reflections, a weekly Slack alert for low-attendance students, new GraphQL queries/mutations for meetings and attendance stats, and a database migration extending Meeting and MeetingAttendance with source, confidence, and scheduling fields.

  • Schema & migration add AttendanceSource enum, new columns on Meeting/MeetingAttendance, and a Project→Meeting relation; the migration is non-destructive with safe defaults.
  • processMentorReflections automation parses mentor survey JSON and auto-creates Meeting/MeetingAttendance records each Monday, but sets scheduledEndAt to weekStart (copy-paste bug) causing every auto-created meeting to have zero duration.
  • MeetingResolver exposes createMeeting, recordMeetingAttendance, and meetingAttendance queries, but the mentor-accessible endpoints do not scope access to the authenticated mentor's own projects, allowing cross-project reads and writes.
  • flaggedStudents in StatsResolver always returns null for the mentor field because the underlying statStudentAttendance query does not include the mentors relation when fetching projects.

Confidence Score: 2/5

Not safe to merge without fixes: mentors can read and overwrite attendance for any project, and the flaggedStudents query returns broken mentor data.

The new mentor-accessible mutations and queries in MeetingResolver perform no project-scoping check, so any authenticated mentor can read attendance records or submit false attendance data for other teams. The flaggedStudents resolver silently returns null for every mentor because the project query it depends on never loads the mentors relation. The auto-meeting creation in processMentorReflections also has a copy-paste error that sets the end time to the start time. These are functional defects on the changed paths rather than theoretical risks.

src/resolvers/Meeting.ts and src/resolvers/Stats.ts need the most attention — the authorization gaps and broken mentor lookup are both there. src/automation/tasks/processMentorReflections.ts needs the scheduledEndAt fix.

Important Files Changed

Filename Overview
src/resolvers/Meeting.ts New resolver for Meeting CRUD and attendance recording; multiple authorization gaps allow mentors to read/write attendance for meetings outside their own projects
src/resolvers/Stats.ts Adds attendance stats and flaggedStudents queries; flaggedStudents always returns null mentor because the upstream query doesn't include the mentors relation on project
src/automation/tasks/processMentorReflections.ts Auto-creates Meeting records from mentor survey responses; scheduledEndAt is incorrectly set to weekStart instead of weekEnd for auto-created meetings
src/automation/tasks/sendAttendanceAlerts.ts Weekly Slack alert task for attendance and reflection issues; contains N+1 query pattern (one DB count per mentor per event)
prisma/schema.prisma Adds AttendanceSource enum, extends Meeting with Slack/project fields, adds source/confidence/metadata to MeetingAttendance, and links Project to Meeting; schema looks correct
prisma/migrations/20260714193312_add_meeting_attendance_tracking/migration.sql SQL migration adding enum, new columns with safe defaults, foreign key, and indexes; migration is non-destructive and looks correct
src/types/AttendanceStats.ts Defines StudentAttendanceStat, MentorReflectionStat, and FlaggedStudent GraphQL object types; definitions are clean
src/email/templates/weeklyAttendanceAlert.md Handlebars email template for weekly attendance reports; contains a hardcoded personal email address in the to: field
src/types/Meeting.ts New GraphQL Meeting type with lazy-loaded relations; implementation follows existing codebase patterns correctly
src/types/MeetingAttendance.ts New GraphQL MeetingAttendance type with source/confidence/metadata fields; follows codebase patterns correctly
scripts/testAttendanceSlack.ts Manual test/seed script for Slack alert integration; logic is sound though it duplicates the event lookup in two places

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Mentor
    participant MeetingResolver
    participant StatsResolver
    participant processMentorReflections
    participant sendAttendanceAlerts
    participant DB as PostgreSQL
    participant Slack

    Mentor->>MeetingResolver: recordMeetingAttendance(meetingId, studentId, attended)
    MeetingResolver->>DB: findFirst MeetingAttendance where meetingId+studentId
    DB-->>MeetingResolver: "existing | null"
    MeetingResolver->>DB: "upsert MeetingAttendance (source=MANUAL)"
    DB-->>MeetingResolver: MeetingAttendance record
    MeetingResolver-->>Mentor: MeetingAttendance

    Note over processMentorReflections: Every Monday 6 AM
    processMentorReflections->>DB: findMany SurveyResponse (mentor, last 8 days)
    DB-->>processMentorReflections: reflections[]
    loop per reflection
        processMentorReflections->>DB: findFirst Meeting (projectId, week window)
        DB-->>processMentorReflections: "meeting | null"
        processMentorReflections->>DB: create Meeting if missing
        loop per student
            processMentorReflections->>DB: "upsert MeetingAttendance (source=MENTOR_REPORT)"
        end
    end

    Note over sendAttendanceAlerts: Every Monday 9 AM
    sendAttendanceAlerts->>DB: findMany active Events
    loop per event
        sendAttendanceAlerts->>DB: findMany Projects (with meetings+attendance)
        loop per project mentor
            sendAttendanceAlerts->>DB: count SurveyResponses for mentor
        end
        sendAttendanceAlerts->>Slack: "postMessage #stats (if issues found)"
    end

    Note over StatsResolver: Admin/Manager query
    StatsResolver->>DB: "findMany Students (with projects->meetings->attendance)"
    DB-->>StatsResolver: students[]
    StatsResolver-->>StatsResolver: flaggedStudents calls statStudentAttendance
    Note right of StatsResolver: mentor field always null - mentors not included in query
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Mentor
    participant MeetingResolver
    participant StatsResolver
    participant processMentorReflections
    participant sendAttendanceAlerts
    participant DB as PostgreSQL
    participant Slack

    Mentor->>MeetingResolver: recordMeetingAttendance(meetingId, studentId, attended)
    MeetingResolver->>DB: findFirst MeetingAttendance where meetingId+studentId
    DB-->>MeetingResolver: "existing | null"
    MeetingResolver->>DB: "upsert MeetingAttendance (source=MANUAL)"
    DB-->>MeetingResolver: MeetingAttendance record
    MeetingResolver-->>Mentor: MeetingAttendance

    Note over processMentorReflections: Every Monday 6 AM
    processMentorReflections->>DB: findMany SurveyResponse (mentor, last 8 days)
    DB-->>processMentorReflections: reflections[]
    loop per reflection
        processMentorReflections->>DB: findFirst Meeting (projectId, week window)
        DB-->>processMentorReflections: "meeting | null"
        processMentorReflections->>DB: create Meeting if missing
        loop per student
            processMentorReflections->>DB: "upsert MeetingAttendance (source=MENTOR_REPORT)"
        end
    end

    Note over sendAttendanceAlerts: Every Monday 9 AM
    sendAttendanceAlerts->>DB: findMany active Events
    loop per event
        sendAttendanceAlerts->>DB: findMany Projects (with meetings+attendance)
        loop per project mentor
            sendAttendanceAlerts->>DB: count SurveyResponses for mentor
        end
        sendAttendanceAlerts->>Slack: "postMessage #stats (if issues found)"
    end

    Note over StatsResolver: Admin/Manager query
    StatsResolver->>DB: "findMany Students (with projects->meetings->attendance)"
    DB-->>StatsResolver: students[]
    StatsResolver-->>StatsResolver: flaggedStudents calls statStudentAttendance
    Note right of StatsResolver: mentor field always null - mentors not included in query
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "First phase for tracking student attenda..." | Re-trigger Greptile

Comment thread src/resolvers/Stats.ts
Comment on lines +275 to +276
for (const stat of attendanceStats.filter((s) => s.isFlagged)) {
const mentor = stat.project?.mentors?.[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code

Comment on lines +147 to +148
scheduledStartAt: weekStart,
scheduledEndAt: weekStart,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
scheduledStartAt: weekStart,
scheduledEndAt: weekStart,
scheduledStartAt: weekStart,
scheduledEndAt: weekEnd,

Fix in Claude Code

Comment thread src/resolvers/Meeting.ts
Comment on lines +89 to +131
@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,
},
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code

Comment thread src/resolvers/Meeting.ts
Comment on lines +133 to +144
@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' },
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code

Comment thread src/resolvers/Meeting.ts
Comment on lines +39 to +61
@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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code

Comment on lines +163 to +175
// Check mentor reflection completion
const mentorReflections = await prisma.surveyResponse.count({
where: {
authorMentorId: mentor.id,
surveyOccurence: {
survey: {
personType: 'MENTOR',
eventId: event.id,
},
},
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 N+1 query pattern per mentor

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.

Fix in Claude Code

Comment on lines +1 to +4
---
to: "akif@codeday.org"
subject: "Weekly Attendance Report for {{ event.name }}"
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Claude Code

@augmentcode

augmentcode Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor
🤖 Augment PR Summary

Summary: Adds meeting attendance tracking to Labs GraphQL, primarily sourced from mentor reflection reporting and used to drive weekly alerting.

Changes:

  • Prisma: adds AttendanceSource; adds source/confidence/metadata to MeetingAttendance; adds projectId plus Slack scheduling fields to Meeting and links Project → meetings.
  • GraphQL: introduces Meeting/MeetingAttendance/MeetingResponse types and inputs, plus a new Meeting resolver with queries and mutations.
  • Stats: adds queries for student attendance %, mentor reflection completion, and a flaggedStudents convenience query.
  • Automation: adds a mentor-reflection processor (Mon 6am) to upsert attendance records and a weekly Slack alert job (Mon 9am).
  • Testing/ops: adds simple task test runners and a manual Slack test script for seeding and previewing alert messages.
  • Email: adds a weekly attendance report template.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 5 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

import { DateTime } from 'luxon';

const DEBUG = makeDebug('automation:tasks:sendAttendanceAlerts');
const ATTENDANCE_ALERT_CHANNEL = 'stats';

@augmentcode augmentcode Bot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread src/resolvers/Stats.ts
const flagged: FlaggedStudent[] = [];

for (const stat of attendanceStats.filter((s) => s.isFlagged)) {
const mentor = stat.project?.mentors?.[0];

@augmentcode augmentcode Bot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread src/resolvers/Meeting.ts
DEBUG(`Recording attendance for meeting ${data.meetingId}, student ${data.studentId}: ${data.attended}`);

// Check for existing attendance record
const existing = await this.prisma.meetingAttendance.findFirst({

@augmentcode augmentcode Bot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 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 },

@augmentcode augmentcode Bot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

visibleAt: weekStart,
dueAt: weekEnd,
scheduledStartAt: weekStart,
scheduledEndAt: weekStart,

@augmentcode augmentcode Bot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant