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
9 changes: 9 additions & 0 deletions .claude/agents/report_writer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: report_writer
description: Keeps report.md (the project's report) updated
model: sonnet
tools:
- Read
- Write
- Bash
---
168 changes: 168 additions & 0 deletions docs/planning/sprint02_review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Sprint 2 Review

**Sprint:** 2
**Dates:** 19–29 December 2025
**Duration:** ~6 working days (including Christmas break)

---

## Summary

Sprint 2 focused on authentication and event management foundations. Completed magic link auth with role-based protection, Resend email integration, and Events CRUD service. AP-17 (admin events page) was partially completed and refined after discovering additional requirements for public events.

---

## Metrics

| Metric | Value |
|--------|-------|
| Planned | 9 tickets, 18 story points |
| Completed | 7 tickets, 12 story points |
| Carried Over | 1 ticket (AP-17), partially done |
| Velocity | 12 points |
| Commits | 67 |
| PRs Merged | 7 (#2–#8) |

---

## Completed Tickets

| Key | Summary | Points | Status |
|-----|---------|--------|--------|
| AP-10 | Create Events table in Airtable | 1 | Done |
| AP-11 | Add Event link field to Attendance table | 1 | Done |
| AP-12 | Create Staff table in Airtable | 1 | Done |
| AP-13 | Set up Resend email integration | 2 | Done |
| AP-14 | Implement magic link authentication | 3 | Done |
| AP-15 | Role-based route protection (SvelteKit hooks) | 2 | Done |
| AP-16 | Event service module (Airtable CRUD) | 2 | Done |

---

## Carried Over

| Key | Summary | Points | Progress |
|-----|---------|--------|----------|
| AP-17 | Admin events page (cohort events, registered users) | 2 | ~25% - load function created |

**Reason:** During implementation, we discovered the need to handle public events (no cohort) and external attendees (not in Airtable). Story was split to deliver incrementally.

---

## New Ticket Created

| Key | Summary | Points |
|-----|---------|--------|
| AP-19 | Public events and external attendee check-in | 3 |

Split from AP-17 to handle:
- Events without cohort (open to all)
- External Name/Email fields on Attendance table
- Public check-in flow for unregistered attendees

---

## Deliverables

**Authentication System:**
- Magic link auth with 15-min JWT tokens
- Separate staff/student login flows
- 90-day session cookies
- `src/lib/server/auth.ts` — token generation/verification
- `src/lib/server/session.ts` — cookie helpers

**Route Protection:**
- `src/hooks.server.ts` — middleware for protected routes
- `/admin/*` → staff only
- `/checkin` → any authenticated user
- Login pages redirect if already authenticated

**Email Integration:**
- Resend configured for magic link delivery
- `src/lib/server/email.ts` — email service
- Environment variable for sender email

**Events Service:**
- Full CRUD operations in `src/lib/airtable/events.ts`
- TypeScript types in `src/lib/types/event.ts`
- Unit tests with Vitest mocks
- Manual test script for verification

**Schema Updates:**
- Events table with Name, DateTime, Cohort, EventType, Survey fields
- Attendance → Event link field
- Staff table with singleCollaborator field
- Updated `docs/schema.md` with relationships diagram

---

## Key Decisions

1. **Separate login flows:** Staff and students have distinct endpoints and pages for clearer UX
2. **Field IDs over names:** All Airtable queries use field IDs to prevent breakage if fields are renamed
3. **Factory pattern:** Airtable clients use factory functions for testability
4. **Story splitting:** AP-17 split into cohort-specific (AP-17) and public events (AP-19) for incremental delivery

---

## Design Decision: External Attendees

Discovered during AP-17 that we need to support:
- Open events (no cohort) visible to everyone
- External attendees not registered in Airtable

**Solution:** Add `External Name` and `External Email` fields to Attendance table.

| Field | Registered User | External User |
|-------|-----------------|---------------|
| Apprentice (link) | ✓ Populated | Empty |
| External Name | Empty | ✓ Populated |
| External Email | Empty | ✓ Populated |

Each attendance record has either an Apprentice link OR External fields (not both).

---

## Risks Identified

| Risk | Mitigation |
|------|------------|
| Public check-in abuse | Rate limiting, time-window validation |
| EventType changes in Airtable | Documented in README, requires code update |
| filterByFormula uses field names | Documented which queries use names vs IDs |

---

## Retrospective Notes

**What went well:**
- Authentication system working end-to-end
- Clean separation of concerns (auth, session, email, events)
- Good test coverage for auth and events modules
- Sprint review caught scope gap early

**What could improve:**
- Initial story estimation missed public events requirement
- Christmas break disrupted momentum

**Action items for Sprint 3:**
- Complete AP-17 (admin events page for cohort events)
- Implement AP-18 (event create/edit/delete UI)
- Begin AP-19 (public events + external check-in) if time permits

---

## Next Sprint Focus

Sprint 3 will deliver the complete event management system and full check-in flow:

**Event Management:**
- AP-17: Admin events page (display, filter by cohort)
- AP-18: Event create/edit/delete UI

**Check-in Flow:**
- AP-19: Public events + external attendee check-in
- Attendance service module (record check-ins to Airtable)
- Check-in UI for registered users (staff/students)

With authentication, route protection, and Events CRUD foundations in place, the project has momentum to deliver end-to-end functionality.
Binary file added docs/planning/sprint03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion docs/scratchpad.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ Show time counter negative and positive

Show mii plaza

integrstion with LUMA
Integration with LUMA

Workshops for external people (emails not on airtable)
90 changes: 81 additions & 9 deletions docs/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,28 @@ Requirements for Apprentice Pulse, derived from project proposal acceptance crit
## Epic 1: Attendance Management

### US-1: Event management
**As** FAC staff
**I want to** create, modify, and delete events via a calendar interface
**As** FAC staff
**I want to** create, modify, and delete events via an admin interface
**So that** I can manage the training schedule and enable attendance tracking

**Acceptance Criteria:**
- Calendar view displays existing events from Airtable
- Create event: select date, cohort, and event type (regular class, workshop, hackathon)
- Edit event: update date, cohort, or event type
- Delete event: remove event from Airtable
- Event syncs to Attendance (Current) table
- Admin page displays existing events from Airtable (list view)
- Filter events by cohort
- Create event: name, date/time, cohort (optional), event type
- Edit event: update any field
- Delete event: remove from Airtable
- 4-digit check-in code auto-generated for each event
- Event type stored for filtering and reporting

**Event Types:**
- Cohort event (has cohort) → only that cohort's apprentices can check in
- Open event (no cohort) → anyone can check in (registered + external)

---

### US-2: Check-in to event
**As an** apprentice
**I want to** register my attendance by tapping an NFC sticker or scanning a QR code
**As an** apprentice
**I want to** register my attendance by tapping an NFC sticker or scanning a QR code
**So that** I can check in quickly without manual roll calls

**Acceptance Criteria:**
Expand All @@ -41,6 +46,73 @@ Requirements for Apprentice Pulse, derived from project proposal acceptance crit
- Attendance record written to Airtable
- Confirmation message displayed

#### Check-in Workflow (Detailed)

**Single URL:** `/checkin` - used for all check-in methods (QR, NFC, direct link)

**Event Types:**
| Type | Has Cohort | Who Can Attend |
|------|------------|----------------|
| Cohort Event | Yes | Only apprentices in that cohort |
| Open Event | No | Anyone (registered + external) |

**Flow 1: Registered User (has session cookie)**
```
/checkin
Show available events:
- User's cohort events
- All open events (no cohort)
One-tap check-in button
Attendance record created (linked to Apprentice)
```

**Flow 2: Registered User (no session)**
```
/checkin
Show login prompt
Enter email → Magic link sent
Click link → Session created (90 days)
Redirect to check-in → Show events → One-tap check-in
```

**Flow 3: External User (public events only)**
```
/checkin
Select "Check in to public event"
Enter 4-digit event code (displayed at venue)
Enter name + email
Attendance record created (External Name/Email fields)
```

**Security:**
| User Type | Security Mechanism |
|-----------|-------------------|
| Registered | Magic link authentication (email verification) |
| External | 4-digit event code (proves physical presence) |

**Edge Case:** If external user enters email that matches an Apprentice record → prompt them to log in instead (keeps attendance data consistent)

**Airtable Attendance Record:**
| Field | Registered User | External User |
|-------|-----------------|---------------|
| Apprentice (link) | ✓ Populated | Empty |
| External Name | Empty | ✓ Populated |
| External Email | Empty | ✓ Populated |
| Event (link) | ✓ | ✓ |
| Check-in Time | ✓ | ✓ |
| Status | ✓ | ✓ |

---

### US-3: Attendance chase email
Expand Down
15 changes: 15 additions & 0 deletions scripts/schema-2025-12-27-12-06-09.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Airtable Schema

## Learners / Events - Apprentice Pulse

Table ID: `tblkbskw4fuTq0E9p`

| Field | ID | Type |
|-------|-----|------|
| Name | `fldMCZijN6TJeUdFR` | singleLineText |
| FAC Cohort | `fldcXDEDkeHvWTnxE` | multipleRecordLinks |
| FAC Cohort (from FAC Cohort) | `fldsLUl1MrhhsVBe7` | multipleLookupValues |
| Date Time | `fld8AkM3EanzZa5QX` | dateTime |
| Survey | `fld9XBHnCWBtZiZah` | url |
| Select | `fldo7fwAsFhkA1icC` | singleSelect |

33 changes: 33 additions & 0 deletions scripts/schema-2025-12-27-14-56-17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Airtable Schema

## Learners / Attendace - Apprentice Pulse

Table ID: `tblkDbhJcuT9TTwFc`

| Field | ID | Type |
|-------|-----|------|
| Id | `fldGdpuw6SoHkQbOs` | autoNumber |
| Apprentice | `fldOyo3hlj9Ht0rfZ` | multipleRecordLinks |
| Cohort | `fldn53kWDE8GHg2Yy` | multipleLookupValues |
| Checkin Time | `fldvXHPmoLlEA8EuN` | dateTime |
| Status | `fldew45fDGpgl1aRr` | singleSelect |
| Event | `fldiHd75LYtopwyN9` | multipleRecordLinks |
| Date Time (from Event) | `fldokfSk68MhJGlm6` | multipleLookupValues |
| FAC Cohort (from FAC Cohort) (from Event) | `fldE783vnY3SLjmh7` | multipleLookupValues |
| FAC Cohort (from Event) | `fldkc9zLJe7NZVAz1` | multipleLookupValues |

## Learners / Events - Apprentice Pulse

Table ID: `tblkbskw4fuTq0E9p`

| Field | ID | Type |
|-------|-----|------|
| Name | `fldMCZijN6TJeUdFR` | singleLineText |
| FAC Cohort | `fldcXDEDkeHvWTnxE` | multipleRecordLinks |
| FAC Cohort (from FAC Cohort) | `fldsLUl1MrhhsVBe7` | multipleLookupValues |
| Date Time | `fld8AkM3EanzZa5QX` | dateTime |
| Survey | `fld9XBHnCWBtZiZah` | url |
| Select | `fldo7fwAsFhkA1icC` | singleSelect |
| Attendace - Apprentice Pulse | `fldcPf53fVfStFZsa` | multipleRecordLinks |
| Name - Date | `fld7POykodV0LGsg1` | formula |

24 changes: 24 additions & 0 deletions src/lib/airtable/airtable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export interface Apprentice {
cohortNumber: string | null;
}

export interface Cohort {
id: string;
name: string;
}

export type UserType = 'staff' | 'student';

export function createAirtableClient(apiKey: string, baseId: string) {
Expand Down Expand Up @@ -131,10 +136,29 @@ export function createAirtableClient(apiKey: string, baseId: string) {
return apprenticeRecords.length > 0;
}

/**
* List all cohorts
*/
async function listCohorts(): Promise<Cohort[]> {
const cohortsTable = base(TABLES.COHORTS);

const records = await cohortsTable
.select({
returnFieldsByFieldId: true,
})
.all();

return records.map(record => ({
id: record.id,
name: (record.get(COHORT_FIELDS.NUMBER) as string) || record.id,
}));
}

return {
getApprenticesByFacCohort,
findUserByEmail,
findStaffByEmail,
findApprenticeByEmail,
listCohorts,
};
}
Loading