+1
-1
.env.example
+1
-1
.env.example
···
+2
-2
package.json
+2
-2
package.json
···
+39
scripts/remove-from-classes.ts
+39
scripts/remove-from-classes.ts
···
···
+215
-15
src/components/admin-classes.ts
+215
-15
src/components/admin-classes.ts
······························
··················+<h2 class="modal-title">${this.editingClassInfo.course_code} - ${this.editingClassInfo.name}</h2>+style="padding: 0.75rem 1.5rem; background: transparent; color: var(--primary); border: none; border-bottom: 2px solid var(--primary); font-weight: 600; cursor: pointer; margin-bottom: -2px;"+style="flex: 1; padding: 0.75rem; border: 2px solid var(--secondary); border-radius: 6px; font-size: 1rem; background: var(--background); color: var(--text);"+style="padding: 0.75rem 1.5rem; background: var(--primary); color: white; border: 2px solid var(--primary); border-radius: 6px; font-weight: 500; cursor: pointer; white-space: nowrap;"+? html`<p style="color: var(--paynes-gray); text-align: center; padding: 2rem;">No sections yet. Add one above.</p>`+<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: color-mix(in srgb, var(--secondary) 30%, transparent); border-radius: 6px;">+style="padding: 0.5rem 1rem; background: transparent; color: red; border: 2px solid red; border-radius: 4px; font-size: 0.875rem; cursor: pointer;"+<div style="display: flex; gap: 0.75rem; justify-content: space-between; padding-top: 1.5rem; border-top: 2px solid var(--secondary);">+style="padding: 0.75rem 1.5rem; background: transparent; color: var(--primary); border: 2px solid var(--primary); border-radius: 6px; font-weight: 500; cursor: pointer;"+style="padding: 0.75rem 1.5rem; background: transparent; color: red; border: 2px solid red; border-radius: 6px; font-weight: 500; cursor: pointer;"+style="padding: 0.75rem 1.5rem; background: var(--primary); color: white; border: 2px solid var(--primary); border-radius: 6px; font-weight: 500; cursor: pointer;"+${this.error ? html`<div style="color: red; margin-top: 1rem; padding: 0.75rem; background: color-mix(in srgb, red 10%, transparent); border-radius: 6px;">${this.error}</div>` : ""}············+<div class="help-text">Comma-separated list of section numbers. Leave blank if no sections.</div>
+60
-11
src/components/class-registration-modal.ts
+60
-11
src/components/class-registration-modal.ts
···························
·····················+style="width: 100%; padding: 0.5rem; border: 2px solid var(--secondary); border-radius: 4px; font-size: 0.875rem; background: var(--background); color: var(--text);"+html`<option value="${s.id}" ?selected=${this.selectedSections.get(cls.id) === s.id}>${s.section_number}</option>`,······
+112
-6
src/components/class-view.ts
+112
-6
src/components/class-view.ts
···························
·····················+style="padding: 0.5rem 0.75rem; border: 1px solid var(--secondary); border-radius: 4px; font-size: 0.875rem; color: var(--text); background: var(--background);"+html`<option value=${s.id} ?selected=${s.id === this.selectedSectionFilter}>${s.section_number}</option>`,······
+1
src/components/classes-overview.ts
+1
src/components/classes-overview.ts
+436
src/components/pending-recordings-view.ts
+436
src/components/pending-recordings-view.ts
···
···+Vote for the best quality recording. The winner will be automatically transcribed when 40% of class votes or after 30 minutes.+<div class="recording-card ${this.userVote === recording.id ? "voted" : ""} ${this.winningRecordingId === recording.id ? "winning" : ""}">
+2
-2
src/components/transcription.ts
+2
-2
src/components/transcription.ts
+360
-66
src/components/upload-recording-modal.ts
+360
-66
src/components/upload-recording-modal.ts
·········
······+formData.append("recording_date", Math.floor(this.selectedFile.lastModified / 1000).toString());···+style="padding: 0.75rem; border: 1px solid var(--secondary); border-radius: 4px; font-size: 0.875rem; color: var(--text); background: var(--background);"+class="meeting-time-button ${this.selectedMeetingTimeId === meeting.id ? "selected" : ""} ${this.detectedMeetingTime === meeting.id ? "detected" : ""}"+<option value="">Use my section ${this.userSection ? `(${this.sections.find((s) => s.id === this.userSection)?.section_number})` : ""}</option>+<div style="background: color-mix(in srgb, var(--primary) 5%, transparent); border-radius: 8px; padding: 1rem;">+<div style="background: var(--secondary); border-radius: 4px; height: 8px; overflow: hidden;">+<div style="background: var(--accent); height: 100%; width: ${this.uploadProgress}%; transition: width 0.3s;"></div>+<button class="btn-cancel" @click=${this.handleClose} ?disabled=${this.uploading || this.submitting}>
+74
-1
src/db/schema.ts
+74
-1
src/db/schema.ts
······
······+CREATE INDEX IF NOT EXISTS idx_recording_votes_transcription_id ON recording_votes(transcription_id);+CREATE INDEX IF NOT EXISTS idx_transcriptions_recording_date ON transcriptions(recording_date);
+3
-1
src/index.test.README.md
+3
-1
src/index.test.README.md
···
···
+353
-747
src/index.test.ts
+353
-747
src/index.test.ts
···-`\n⚠️ Test server not running on port ${TEST_PORT}. Start it with:\n PORT=${TEST_PORT} bun run src/index.ts\n Then run tests in another terminal.\n`,···-"DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",-"DELETE FROM passkeys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",-"DELETE FROM transcriptions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",-"DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%'",·············································································································································
···+// Dummy env vars to pass startup validation (won't be used due to SKIP_EMAILS/SKIP_POLAR_SYNC)················································································································································
+877
-409
src/index.ts
+877
-409
src/index.ts
···························-"SELECT status FROM subscriptions WHERE user_id = ? AND status IN ('active', 'trialing', 'past_due') ORDER BY created_at DESC LIMIT 1",····························································-"INSERT INTO transcriptions (id, user_id, class_id, meeting_time_id, filename, original_filename, status) VALUES (?, ?, ?, ?, ?, ?, ?)",·····································································
···························+"SELECT status FROM subscriptions WHERE user_id = ? AND status IN ('active', 'trialing', 'past_due') ORDER BY created_at DESC LIMIT 1",····························································+"INSERT INTO transcriptions (id, user_id, class_id, meeting_time_id, section_id, filename, original_filename, status, recording_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",··································································+"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://hostedboringavatars.vercel.app; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none';",···
+1
-1
src/lib/api-response-format.test.ts
+1
-1
src/lib/api-response-format.test.ts
+55
src/lib/audio-metadata.integration.test.ts
+55
src/lib/audio-metadata.integration.test.ts
···
···+await Bun.$`ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 1 -metadata creation_time=${creationTime} -y ${testAudioPath}`.quiet();
+128
src/lib/audio-metadata.test.ts
+128
src/lib/audio-metadata.test.ts
···
···
+144
src/lib/audio-metadata.ts
+144
src/lib/audio-metadata.ts
···
···+`[AudioMetadata] Extracted creation date from metadata: ${date.toISOString()} from ${filePath}`,+`[AudioMetadata] No meeting time found matching ${dayName} in available options: ${meetingTimes.map((mt) => mt.label).join(", ")}`,
+34
src/lib/auth.test.ts
+34
src/lib/auth.test.ts
···
+26
-1
src/lib/auth.ts
+26
-1
src/lib/auth.ts
·········
·········
+111
-8
src/lib/classes.ts
+111
-8
src/lib/classes.ts
···························-`SELECT id, user_id, meeting_time_id, filename, original_filename, status, progress, error_message, created_at, updated_at······
··················+"INSERT OR IGNORE INTO class_members (class_id, user_id, section_id, enrolled_at) VALUES (?, ?, ?, ?)",·········+`SELECT id, user_id, meeting_time_id, section_id, filename, original_filename, status, progress, error_message, created_at, updated_at······
+3
-3
src/lib/cursor.test.ts
+3
-3
src/lib/cursor.test.ts
+12
-9
src/lib/email-verification.test.ts
+12
-9
src/lib/email-verification.test.ts
············
············
+8
src/lib/email.ts
+8
src/lib/email.ts
···
···+`[Email] SKIPPED (test mode): "${options.subject}" to ${typeof options.to === "string" ? options.to : options.to.email}`,
+3
-13
src/lib/pagination.test.ts
+3
-13
src/lib/pagination.test.ts
······"INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)","INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)","INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)",
······"INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)","INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)","INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)",
+2
-2
src/lib/subscription-routes.test.ts
+2
-2
src/lib/subscription-routes.test.ts
······
······
+227
src/lib/voting.ts
+227
src/lib/voting.ts
···
···+let query = `SELECT id, user_id, filename, original_filename, vote_count, created_at, section_id+`[Voting] Auto-submitting ${topRecording.id} - reached ${topRecording.vote_count}/${voteThreshold} votes (40% threshold)`,
+10
src/lib/vtt-cleaner.test.ts
+10
src/lib/vtt-cleaner.test.ts
···
+6
src/lib/vtt-cleaner.ts
+6
src/lib/vtt-cleaner.ts
···
-1
src/pages/admin.html
-1
src/pages/admin.html
···-<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
···
+104
-89
src/pages/admin.ts
+104
-89
src/pages/admin.ts
···
···
-1
src/pages/checkout.html
-1
src/pages/checkout.html
···-<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
···
-1
src/pages/class.html
-1
src/pages/class.html
···-<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
···
-1
src/pages/classes.html
-1
src/pages/classes.html
···-<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
···
-1
src/pages/index.html
-1
src/pages/index.html
···-<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
···
+12
-8
src/pages/index.ts
+12
-8
src/pages/index.ts
···
···
-1
src/pages/reset-password.html
-1
src/pages/reset-password.html
···-<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
···
+4
-4
src/pages/reset-password.ts
+4
-4
src/pages/reset-password.ts
···
···
-1
src/pages/settings.html
-1
src/pages/settings.html
···-<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
···
-1
src/pages/transcribe.html
-1
src/pages/transcribe.html
···-<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
···
+64
-64
src/styles/admin.css
+64
-64
src/styles/admin.css
···
···
+41
-41
src/styles/index.css
+41
-41
src/styles/index.css
···
···
+4
-4
src/styles/reset-password.css
+4
-4
src/styles/reset-password.css
+1
-1
src/styles/settings.css
+1
-1
src/styles/settings.css
+13
-13
src/styles/transcribe.css
+13
-13
src/styles/transcribe.css
···
···