commits
Implement cursor-based pagination with base64url-encoded cursors for:
- GET /api/transcriptions (user's transcriptions)
- GET /api/admin/transcriptions (all transcriptions)
- GET /api/admin/users (all users with stats)
- GET /api/classes (user's classes)
Response format: { data: [...], pagination: { limit, hasMore, nextCursor } }
Query params: ?limit=50&cursor=<base64url-string>
Cursors are opaque, URL-safe, short strings (20-40 chars) that prevent
page drift and support efficient pagination at any scale.
Implementation:
- Created src/lib/cursor.ts for encoding/decoding cursors
- Updated getAllTranscriptions, getAllUsersWithStats, getClassesForUser
- Default limit: 50, max: 100
- Comprehensive test coverage (23 tests)
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
- Session cleanup: Run every 15 minutes instead of hourly (#26)
- Health check: Add detailed status for database, whisper, and storage (#25)
- Admin email change: Require verification unless skipVerification flag set (#36)
- Transcription selection: Validate existence and status before selecting (#28)
These changes improve system monitoring, security, and error handling.
Added detailed documentation for tangled-cli including:
- Authentication and installation
- Creating, listing, showing, editing issues
- Commenting on issues
- Repository commands
- Priority label filtering
- JSON output format
- Known issues with the CLI
Changed from semi-realistic looking values to clearly fake
placeholders using 'paste_your_*_here' format to prevent
confusion with real credentials.
Fixes issue #24
Check if class exists when toggling archive status and throw error
if not found. Prevents silent failures when archiving non-existent
classes.
Fixes issue #28 (partial)
- File uploads: 20/hour
- User settings (name, avatar, notifications): 10 per 5 min
- Session deletion: 20/hour
- Passkey operations: 10 per 5 min (auth), 10/hour (update/delete)
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
Implements graceful shutdown on SIGTERM/SIGINT that:
- Stops accepting new requests via server.stop()
- Closes all active SSE streams (sync will reconnect on restart)
- Stops transcription service streams to Murmur
- Clears cleanup intervals (sessions, sync, files)
- Closes database connections cleanly
Prevents database corruption and ensures clean shutdown.
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
🪻 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
The classes tests were creating a separate test database file but the
functions being tested still used the production database, causing tests
to fail when production data existed.
Changed to match the pattern used in other tests (auth.test.ts, etc):
- Use production database
- Track created resources in beforeEach/afterEach
- Clean up all test data after each test
- Helper functions to create and track test users/classes
💖 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
- Migrate biome config to version 2.3.2
- Replace explicit any types with proper interfaces
- Remove non-null assertions with proper null checks
- Apply all auto-formatting fixes
💖 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
The audio endpoint now accepts both "pending" and "completed" status, allowing admins to preview pending recordings before transcription.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Replaced sidebar navigation with horizontal tab layout matching the admin panel design for better consistency and cleaner appearance.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Replaced intrusive browser confirm dialogs with a progressive three-click delete system:
- First click: "Delete" → "Are you sure?"
- Second click: "Are you sure?" → "Final warning!"
- Third click: "Final warning!" → Actually deletes
Features:
- 1 second timeout between clicks before resetting
- State tracked per item (can't accidentally delete wrong item)
- Visual feedback through button text changes
- Works for both class deletion and waitlist deletion
- No blocking dialogs, cleaner UX
Implementation:
- State management with deleteState tracking id, type, clicks, and timeout
- Automatic cleanup with setTimeout
- Separate handlers for performing actual deletion
- Button text updates reactively based on state
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Replaced text-based meeting time inputs with an intuitive visual day picker:
- New meeting-time-picker component with clickable day buttons (Mon-Sun)
- Click to select/deselect days when class meets
- Selected days highlight with primary color
- No time inputs needed - just tracks which days
- Auto-generates labels from selected days (e.g., "Monday", "Tuesday")
Student workflow:
- Waitlist form uses meeting-time-picker
- Click days when their class meets
- Data stored as JSON array of day objects
Admin workflow:
- Approval modal shows all editable fields (not just meeting times)
- Course code, name, professor, semester, year all editable
- Meeting time picker pre-filled with student's selections
- Admin can edit any field before creating class
- Error messages now display inside modal (not behind it)
- Better error logging for debugging
Technical improvements:
- Fixed SQL query to remove section column reference
- Proper Lit reactivity with updated() lifecycle
- Meeting times load correctly from waitlist data
- Clean separation between MeetingTime interface and picker component
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Students can now request classes that don't exist yet:
- Search for classes, if not found, request it via waitlist form
- Waitlist form collects: course code, name, professor, semester, year, meeting times, and optional additional info
- Meeting times field allows multiple entries (e.g., "Monday Lecture", "Wednesday Lab")
- Pressing Enter in meeting time field adds another field instead of submitting
Admin workflow for waitlist management:
- New "Classes" tab in admin panel with two sub-tabs: Classes and Waitlist
- Waitlist shows all pending requests with badge count
- "Approve & Create Class" opens modal pre-filled with student's request
- Admin can edit meeting times before creating the class
- Creates class with meeting times, removes from waitlist, and switches to Classes tab
- Can also delete waitlist requests
Additional changes:
- Removed section field completely (not used, meeting times are more meaningful)
- Database migrations: v3 (waitlist table), v4 (meeting_times column), v5 (drop section)
- Meeting times stored as JSON array in waitlist, created as separate records when approved
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Students now search for classes and select from results:
- Added section column to classes table (migration v2)
- Search by course code returns all matching classes
- Results show professor, section, semester, year
- Students pick specific section/professor when joining
- Updated Class interface to include section field
- Replaced joinClassByCode with searchClassesByCourseCode + joinClass
- New GET /api/classes/search?q=<query> endpoint
- Updated POST /api/classes/join to accept class_id
- Redesigned modal with search form and clickable result cards
Better UX for multi-section courses with different professors.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Students can now join classes by entering a class code:
- Created class-registration-modal component with form
- Added joinClassByCode() function in classes.ts
- Added POST /api/classes/join API endpoint
- Integrated modal into classes-overview component
- Classes are joined by their ID (used as the class code)
- Validates class exists, not archived, and user not already enrolled
UI shows "+ Register for Class" card that opens the modal.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Updated the auth component user menu to link to /classes instead of
/transcribe, aligning with the new class-based workflow.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Replaced table-based layouts with modern card components for better UX:
- Transcriptions tab now uses admin-transcriptions component
- Users tab now uses admin-users component
- Both tabs feature search, filtering, and click-to-view modals
- Removed ~500 lines of duplicate table-rendering code
- Cleaned up CSS (removed table/search/sort styles)
- Simplified JavaScript to just handle stats and modal events
All three admin tabs now have consistent card-based design.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
- Replace table layout with card-based grid layout
- Add embedded audio player to each card for preview
- Organize metadata in labeled sections (Class, Meeting Time, Uploader, Upload Date)
- Improve visual hierarchy with larger filename and better spacing
- Make approve/delete buttons full-width for easier interaction
- Audio player uses /api/transcriptions/:id/audio endpoint
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
- Create admin-pending-recordings component
- Display all pending recordings across all classes
- Show class info (course code, name), meeting time, uploader, upload date
- Approve & Transcribe button to select recordings for transcription
- Delete button to remove unwanted recordings
- Add new "Pending Recordings" tab as first tab in admin panel
- Fetch recordings from all classes and aggregate them
- Call PUT /api/transcripts/:id/select to approve recordings
- Reload data after approve/delete actions
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Implement cursor-based pagination with base64url-encoded cursors for:
- GET /api/transcriptions (user's transcriptions)
- GET /api/admin/transcriptions (all transcriptions)
- GET /api/admin/users (all users with stats)
- GET /api/classes (user's classes)
Response format: { data: [...], pagination: { limit, hasMore, nextCursor } }
Query params: ?limit=50&cursor=<base64url-string>
Cursors are opaque, URL-safe, short strings (20-40 chars) that prevent
page drift and support efficient pagination at any scale.
Implementation:
- Created src/lib/cursor.ts for encoding/decoding cursors
- Updated getAllTranscriptions, getAllUsersWithStats, getClassesForUser
- Default limit: 50, max: 100
- Comprehensive test coverage (23 tests)
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
- Session cleanup: Run every 15 minutes instead of hourly (#26)
- Health check: Add detailed status for database, whisper, and storage (#25)
- Admin email change: Require verification unless skipVerification flag set (#36)
- Transcription selection: Validate existence and status before selecting (#28)
These changes improve system monitoring, security, and error handling.
Implements graceful shutdown on SIGTERM/SIGINT that:
- Stops accepting new requests via server.stop()
- Closes all active SSE streams (sync will reconnect on restart)
- Stops transcription service streams to Murmur
- Clears cleanup intervals (sessions, sync, files)
- Closes database connections cleanly
Prevents database corruption and ensures clean shutdown.
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
The classes tests were creating a separate test database file but the
functions being tested still used the production database, causing tests
to fail when production data existed.
Changed to match the pattern used in other tests (auth.test.ts, etc):
- Use production database
- Track created resources in beforeEach/afterEach
- Clean up all test data after each test
- Helper functions to create and track test users/classes
💖 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Replaced intrusive browser confirm dialogs with a progressive three-click delete system:
- First click: "Delete" → "Are you sure?"
- Second click: "Are you sure?" → "Final warning!"
- Third click: "Final warning!" → Actually deletes
Features:
- 1 second timeout between clicks before resetting
- State tracked per item (can't accidentally delete wrong item)
- Visual feedback through button text changes
- Works for both class deletion and waitlist deletion
- No blocking dialogs, cleaner UX
Implementation:
- State management with deleteState tracking id, type, clicks, and timeout
- Automatic cleanup with setTimeout
- Separate handlers for performing actual deletion
- Button text updates reactively based on state
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Replaced text-based meeting time inputs with an intuitive visual day picker:
- New meeting-time-picker component with clickable day buttons (Mon-Sun)
- Click to select/deselect days when class meets
- Selected days highlight with primary color
- No time inputs needed - just tracks which days
- Auto-generates labels from selected days (e.g., "Monday", "Tuesday")
Student workflow:
- Waitlist form uses meeting-time-picker
- Click days when their class meets
- Data stored as JSON array of day objects
Admin workflow:
- Approval modal shows all editable fields (not just meeting times)
- Course code, name, professor, semester, year all editable
- Meeting time picker pre-filled with student's selections
- Admin can edit any field before creating class
- Error messages now display inside modal (not behind it)
- Better error logging for debugging
Technical improvements:
- Fixed SQL query to remove section column reference
- Proper Lit reactivity with updated() lifecycle
- Meeting times load correctly from waitlist data
- Clean separation between MeetingTime interface and picker component
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Students can now request classes that don't exist yet:
- Search for classes, if not found, request it via waitlist form
- Waitlist form collects: course code, name, professor, semester, year, meeting times, and optional additional info
- Meeting times field allows multiple entries (e.g., "Monday Lecture", "Wednesday Lab")
- Pressing Enter in meeting time field adds another field instead of submitting
Admin workflow for waitlist management:
- New "Classes" tab in admin panel with two sub-tabs: Classes and Waitlist
- Waitlist shows all pending requests with badge count
- "Approve & Create Class" opens modal pre-filled with student's request
- Admin can edit meeting times before creating the class
- Creates class with meeting times, removes from waitlist, and switches to Classes tab
- Can also delete waitlist requests
Additional changes:
- Removed section field completely (not used, meeting times are more meaningful)
- Database migrations: v3 (waitlist table), v4 (meeting_times column), v5 (drop section)
- Meeting times stored as JSON array in waitlist, created as separate records when approved
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Students now search for classes and select from results:
- Added section column to classes table (migration v2)
- Search by course code returns all matching classes
- Results show professor, section, semester, year
- Students pick specific section/professor when joining
- Updated Class interface to include section field
- Replaced joinClassByCode with searchClassesByCourseCode + joinClass
- New GET /api/classes/search?q=<query> endpoint
- Updated POST /api/classes/join to accept class_id
- Redesigned modal with search form and clickable result cards
Better UX for multi-section courses with different professors.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Students can now join classes by entering a class code:
- Created class-registration-modal component with form
- Added joinClassByCode() function in classes.ts
- Added POST /api/classes/join API endpoint
- Integrated modal into classes-overview component
- Classes are joined by their ID (used as the class code)
- Validates class exists, not archived, and user not already enrolled
UI shows "+ Register for Class" card that opens the modal.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
Replaced table-based layouts with modern card components for better UX:
- Transcriptions tab now uses admin-transcriptions component
- Users tab now uses admin-users component
- Both tabs feature search, filtering, and click-to-view modals
- Removed ~500 lines of duplicate table-rendering code
- Cleaned up CSS (removed table/search/sort styles)
- Simplified JavaScript to just handle stats and modal events
All three admin tabs now have consistent card-based design.
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
- Replace table layout with card-based grid layout
- Add embedded audio player to each card for preview
- Organize metadata in labeled sections (Class, Meeting Time, Uploader, Upload Date)
- Improve visual hierarchy with larger filename and better spacing
- Make approve/delete buttons full-width for easier interaction
- Audio player uses /api/transcriptions/:id/audio endpoint
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
- Create admin-pending-recordings component
- Display all pending recordings across all classes
- Show class info (course code, name), meeting time, uploader, upload date
- Approve & Transcribe button to select recordings for transcription
- Delete button to remove unwanted recordings
- Add new "Pending Recordings" tab as first tab in admin panel
- Fetch recordings from all classes and aggregate them
- Call PUT /api/transcripts/:id/select to approve recordings
- Reload data after approve/delete actions
💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>