+24
-10
.env.example
+24
-10
.env.example
······+DKIM_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nPASTE_YOUR_DKIM_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----"
+117
CRUSH.md
+117
CRUSH.md
···**IMPORTANT**: Do NOT commit changes until the user explicitly asks you to commit. Always wait for user verification that changes are working correctly before making commits.+**CRITICAL**: Always use `process.env.ORIGIN` for generating URLs in emails and links, NOT hardcoded domains.+- `ORIGIN` - The public URL of the application (e.g., `https://thistle.app` or `http://localhost:3000`)+**Never hardcode domain names** like `https://thistle.app` in code - always use `process.env.ORIGIN`.···+This project uses [Tangled](https://tangled.org) for issue tracking via the `tangled-cli` tool.+tangled-cli issue create --repo "thistle" --title "Issue title" --label "bug" --label "priority:high" --body "Issue description"+**Note:** The repo name for this project is `thistle` (resolves to `dunkirk.sh/thistle` in Tangled). Labels are supported but need to be created in the repository first.+- The CLI may have decoding issues with some API responses (missing `createdAt` field). If `tangled-cli issue list` fails, you can access issues via the web interface at https://tangled.org/dunkirk.sh/thistle
+10
LICENSE.md
+10
LICENSE.md
···+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.+No licensee or downstream recipient may use the Software (including any modified or derivative versions) to directly compete with the original Licensor by offering it to third parties as a hosted, managed, or Software-as-a-Service (SaaS) product or cloud service where the primary value of the service is the functionality of the Software itself.+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+2
-1
README.md
+2
-1
README.md
······-<a href="https://github.com/taciturnaxolotl/thistle/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=License&message=MIT&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a>+<a href="https://github.com/taciturnaxolotl/thistle/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=License&message=O'Saasy&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a>
-264
docs/CLASS_SYSTEM_SPEC.md
-264
docs/CLASS_SYSTEM_SPEC.md
···-Restructure Thistle from individual transcript management to class-based transcript organization. Users will manage transcripts grouped by classes, with scheduled meeting times and selective transcription.
+28
-28
package.json
+28
-28
package.json
···
+19
-1
public/favicon/site.webmanifest
+19
-1
public/favicon/site.webmanifest
···-{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
+17
scripts/clear-rate-limits.ts
+17
scripts/clear-rate-limits.ts
···
+39
scripts/remove-from-classes.ts
+39
scripts/remove-from-classes.ts
···
+70
scripts/send-test-emails.ts
+70
scripts/send-test-emails.ts
···
+260
-29
src/components/admin-classes.ts
+260
-29
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>
+36
-13
src/components/admin-pending-recordings.ts
+36
-13
src/components/admin-pending-recordings.ts
··················
+30
-10
src/components/admin-transcriptions.ts
+30
-10
src/components/admin-transcriptions.ts
··················
+94
-44
src/components/admin-users.ts
+94
-44
src/components/admin-users.ts
···············-private handleRevokeClick(userId: number, email: string, subscriptionId: string, event: Event) {···-private async performRevokeSubscription(userId: number, email: string, subscriptionId: string) {···-alert(`Failed to revoke subscription: ${error instanceof Error ? error.message : "Unknown error"}`);·········// Don't open modal if clicking on delete button, revoke button, sync button, or role select············-<div class="user-card ${u.id === 0 ? 'system' : ''}" @click=${(e: Event) => this.handleCardClick(u.id, e)}>+<div class="user-card ${u.id === 0 ? "system" : ""}" @click=${(e: Event) => this.handleCardClick(u.id, e)}>······-? html`<span class="subscription-badge ${u.subscription_status.toLowerCase()}">${u.subscription_status}</span>`+? html`<span class="subscription-badge ${u.subscription_status.toLowerCase()}">${u.subscription_status}</span>`···-? html`<div style="color: var(--paynes-gray); font-size: 0.875rem; padding: 0.5rem;">System account cannot be modified</div>`+? html`<div style="color: var(--paynes-gray); font-size: 0.875rem; padding: 0.5rem;">System account cannot be modified</div>`···?disabled=${!u.subscription_status || !u.subscription_id || this.revokingSubscriptions.has(u.id)}···
+273
-4
src/components/auth.ts
+273
-4
src/components/auth.ts
·····················+${this.needsEmailVerification ? "Verify Email" : this.needsRegistration ? "Create Account" : "Sign In"}···
+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>`,······
+123
-13
src/components/class-view.ts
+123
-13
src/components/class-view.ts
·····················<div style="background: color-mix(in srgb, var(--accent) 10%, transparent); border: 1px solid var(--accent); border-radius: 8px; padding: 1.5rem; margin: 2rem 0; text-align: center;"><p style="margin: 0 0 1rem 0; color: var(--text); opacity: 0.8;">You need an active subscription to upload and view transcriptions.</p><a href="/settings?tab=billing" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--accent); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; transition: opacity 0.2s;">Subscribe Now</a>+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>`,······<p>${this.searchQuery ? "Try a different search term" : "Upload a recording to get started!"}</p>···
+21
-7
src/components/classes-overview.ts
+21
-7
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" : ""}">
+335
src/components/reset-password-form.ts
+335
src/components/reset-password-form.ts
···
+10
-5
src/components/transcription.ts
+10
-5
src/components/transcription.ts
······<div style="background: color-mix(in srgb, var(--accent) 10%, transparent); border: 1px solid var(--accent); border-radius: 8px; padding: 1.5rem; margin-bottom: 2rem; text-align: center;"><h3 style="margin: 0 0 0.5rem 0; color: var(--text);">Subscribe to Upload Transcriptions</h3><p style="margin: 0 0 1rem 0; color: var(--text); opacity: 0.8;">You need an active subscription to upload and transcribe audio files.</p><a href="/settings?tab=billing" style="display: inline-block; padding: 0.75rem 1.5rem; background: var(--accent); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; transition: opacity 0.2s;">Subscribe Now</a><div class="upload-area ${this.dragOver ? "drag-over" : ""} ${!canUpload ? "disabled" : ""}"
+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}>
+44
-31
src/components/user-modal.ts
+44
-31
src/components/user-modal.ts
···············-"Are you sure you want to change this user's password? This will log them out of all devices.",+"Password reset email sent successfully. The user will receive a link to set a new password.",···<input type="email" id="new-email" class="form-input" placeholder="Enter new email" value=${this.user.email}>+<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.875rem;">+<p class="info-text">Send a password reset email to this user. They will receive a secure link to set a new password.</p>-<input type="password" id="new-password" class="form-input" placeholder="Enter new password (min 8 characters)">
+339
-43
src/components/user-settings.ts
+339
-43
src/components/user-settings.ts
··········································+${this.pendingEmailChange ? html`<br><strong>New email:</strong> ${this.pendingEmailChange}` : ""}·································Opens the customer portal where you can update payment methods, view invoices, and manage your subscription.·····················
+125
-45
src/db/schema.ts
+125
-45
src/db/schema.ts
···············CREATE INDEX IF NOT EXISTS idx_transcriptions_whisper_job_id ON transcriptions(whisper_job_id);+CREATE INDEX IF NOT EXISTS idx_transcriptions_meeting_time_id ON transcriptions(meeting_time_id);······+CREATE INDEX IF NOT EXISTS idx_verification_tokens_user_id ON email_verification_tokens(user_id);+CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON password_reset_tokens(user_id);+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);
+4
-2
src/index.test.README.md
+4
-2
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`,+// Dummy env vars to pass startup validation (won't be used due to SKIP_EMAILS/SKIP_POLAR_SYNC)···-"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%'",·············································································································································
+1980
-424
src/index.ts
+1980
-424
src/index.ts
························`INSERT INTO subscriptions (id, user_id, customer_id, status, current_period_start, current_period_end, cancel_at_period_end, canceled_at, updated_at)···-`[Sync] Linked ${subscriptions.result.items.length} subscription(s) to user ${userId} (${email})`,+`[Sync] Linked ${currentSubscriptions.length} current subscription(s) to user ${userId} (${email})`,···············-"Set-Cookie": `session=${sessionId}; HttpOnly; Secure; Path=/; Max-Age=${7 * 24 * 60 * 60}; SameSite=Lax`,·········+"Set-Cookie": `session=${sessionId}; HttpOnly; Secure; Path=/; Max-Age=${7 * 24 * 60 * 60}; SameSite=Lax`,+"Set-Cookie": `session=${sessionId}; HttpOnly; Secure; Path=/; Max-Age=${7 * 24 * 60 * 60}; SameSite=Lax`,···-"SELECT status FROM subscriptions WHERE user_id = ? AND status IN ('active', 'trialing', 'past_due') ORDER BY created_at DESC LIMIT 1",+"SELECT status FROM subscriptions WHERE user_id = ? AND status IN ('active', 'trialing', 'past_due') ORDER BY created_at DESC LIMIT 1",····························································-"SELECT id, filename, original_filename, class_id, status, progress, created_at FROM transcriptions WHERE user_id = ? ORDER BY created_at DESC",·········-"INSERT INTO transcriptions (id, user_id, class_id, meeting_time_id, filename, original_filename, status) VALUES (?, ?, ?, ?, ?, ?, ?)",+"INSERT INTO transcriptions (id, user_id, class_id, meeting_time_id, section_id, filename, original_filename, status, recording_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",···+return Response.json(result.data); // Return just the array for now, can add pagination UI later···+return Response.json(result.data); // Return just the array for now, can add pagination UI later··························································································+"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';",
+136
src/lib/api-response-format.test.ts
+136
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
···
+428
-67
src/lib/auth.ts
+428
-67
src/lib/auth.ts
······"INSERT INTO sessions (id, user_id, ip_address, user_agent, expires_at) VALUES (?, ?, ?, ?, ?)",···+"SELECT id, status, cancel_at_period_end FROM subscriptions WHERE user_id = ? ORDER BY created_at DESC LIMIT 1",···+`[User Delete] Skipping cancellation for subscription ${subscription.id} (status: ${subscription.status}, cancel_at_period_end: ${subscription.cancel_at_period_end})`,······+"INSERT INTO email_change_tokens (id, user_id, new_email, token, expires_at) VALUES (?, ?, ?, ?, ?)",······-LEFT JOIN subscriptions s ON u.id = s.user_id AND s.status IN ('active', 'trialing', 'past_due')+LEFT JOIN subscriptions s ON u.id = s.user_id AND s.status IN ('active', 'trialing', 'past_due')+LEFT JOIN subscriptions s ON u.id = s.user_id AND s.status IN ('active', 'trialing', 'past_due')
+7
-7
src/lib/classes.test.ts
+7
-7
src/lib/classes.test.ts
···
+248
-22
src/lib/classes.ts
+248
-22
src/lib/classes.ts
··················+"INSERT OR IGNORE INTO class_members (class_id, user_id, section_id, enrolled_at) VALUES (?, ?, ?, ?)",·········-`SELECT id, user_id, meeting_time_id, filename, original_filename, status, progress, error_message, created_at, updated_at+`SELECT id, user_id, meeting_time_id, section_id, filename, original_filename, status, progress, error_message, created_at, updated_at······
-1
src/lib/client-auth.ts
-1
src/lib/client-auth.ts
+1
-2
src/lib/crypto-fallback.ts
+1
-2
src/lib/crypto-fallback.ts
···
+117
src/lib/cursor.test.ts
+117
src/lib/cursor.test.ts
···
+92
src/lib/cursor.ts
+92
src/lib/cursor.ts
···
+116
src/lib/email-change.test.ts
+116
src/lib/email-change.test.ts
···
+297
src/lib/email-templates.ts
+297
src/lib/email-templates.ts
···+This code will expire in 24 hours. Enter it in the verification dialog after you login, or click the button below:+<a href="${verifyLink}" class="button" style="display: inline-block; background-color: #ef8354; color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 500; font-size: 1rem; margin: 0; border: 2px solid #ef8354;">Verify Email</a>+<p>We received a request to reset your password. Click the button below to create a new password.</p>+<a href="${options.resetLink}" class="button" style="display: inline-block; background-color: #ef8354; color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 500; font-size: 1rem; margin: 1rem 0; border: 2px solid #ef8354;">Reset Password</a>+<a href="${options.resetLink}" style="color: #4f5d75; word-break: break-all;">${options.resetLink}</a>+<a href="${options.transcriptLink}" class="button" style="display: inline-block; background-color: #ef8354; color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 500; font-size: 1rem; margin: 0; border: 2px solid #ef8354;">View Transcript</a>+<a href="${options.verifyLink}" class="button" style="display: inline-block; background-color: #ef8354; color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 500; font-size: 1rem; margin: 0; border: 2px solid #ef8354;">Verify Email Change</a>+<a href="${options.verifyLink}" style="color: #4f5d75; word-break: break-all;">${options.verifyLink}</a>+<p>If you didn't request this change, please ignore this email and your email address will remain unchanged.</p>
+161
src/lib/email-verification.test.ts
+161
src/lib/email-verification.test.ts
···
+97
src/lib/email.ts
+97
src/lib/email.ts
···+`[Email] SKIPPED (test mode): "${options.subject}" to ${typeof options.to === "string" ? options.to : options.to.email}`,
+326
src/lib/pagination.test.ts
+326
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 transcriptions (id, user_id, filename, original_filename, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",+"INSERT INTO classes (id, course_code, name, professor, semester, year) VALUES (?, ?, ?, ?, ?, ?)",+"INSERT INTO classes (id, course_code, name, professor, semester, year) VALUES (?, ?, ?, ?, ?, ?)",
+20
src/lib/rate-limit.ts
+20
src/lib/rate-limit.ts
···
+2
-2
src/lib/subscription-routes.test.ts
+2
-2
src/lib/subscription-routes.test.ts
······
+21
-6
src/lib/transcription.ts
+21
-6
src/lib/transcription.ts
······
+118
src/lib/validation.test.ts
+118
src/lib/validation.test.ts
···
+223
src/lib/validation.ts
+223
src/lib/validation.ts
···+/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
+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
···
+8
-8
src/lib/vtt-cleaner.ts
+8
-8
src/lib/vtt-cleaner.ts
···-"[VTTCleaner] LLM configuration incomplete (need LLM_API_KEY, LLM_API_BASE_URL, LLM_MODEL), returning uncleaned VTT",
+4
-250
src/pages/admin.html
+4
-250
src/pages/admin.html
···<link rel="icon" type="image/png" sizes="16x16" href="../../public/favicon/favicon-16x16.png">······
+151
src/pages/admin.ts
+151
src/pages/admin.ts
···
+2
-85
src/pages/index.html
+2
-85
src/pages/index.html
···<link rel="icon" type="image/png" sizes="16x16" href="../../public/favicon/favicon-16x16.png">···
+14
src/pages/index.ts
+14
src/pages/index.ts
···
+36
src/pages/reset-password.html
+36
src/pages/reset-password.html
···
+10
src/pages/reset-password.ts
+10
src/pages/reset-password.ts
···
+1
-6
src/pages/settings.html
+1
-6
src/pages/settings.html
···<link rel="icon" type="image/png" sizes="16x16" href="../../public/favicon/favicon-16x16.png">
+3
-21
src/pages/transcribe.html
+3
-21
src/pages/transcribe.html
···<link rel="icon" type="image/png" sizes="16x16" href="../../public/favicon/favicon-16x16.png">···-<a href="/classes" style="color: var(--paynes-gray); text-decoration: none; font-size: 0.875rem;">
+120
src/styles/admin.css
+120
src/styles/admin.css
···
+71
src/styles/index.css
+71
src/styles/index.css
···
+6
src/styles/reset-password.css
+6
src/styles/reset-password.css
+27
src/styles/transcribe.css
+27
src/styles/transcribe.css
···