🪻 distributed transcription service thistle.dunkirk.sh

chore: fix biome lint issues and format

dunkirk.sh b30c6519 d024a754

verified
+27 -16
src/index.ts
···
console.warn(
"[Startup] ORIGIN not set, defaulting to http://localhost:3000",
);
-
console.warn(
-
"[Startup] Set ORIGIN in production for correct email links",
-
);
+
console.warn("[Startup] Set ORIGIN in production for correct email links");
}
console.log("[Startup] Environment variable validation passed");
···
}),
});
-
return Response.json({ success: true, message: "Verification email sent" });
+
return Response.json({
+
success: true,
+
message: "Verification email sent",
+
});
} catch (error) {
return handleError(error);
}
···
await updateUserPassword(userId, password);
consumePasswordResetToken(token);
-
return Response.json({ success: true, message: "Password reset successfully" });
+
return Response.json({
+
success: true,
+
message: "Password reset successfully",
+
});
} catch (error) {
console.error("[Email] Reset password error:", error);
return Response.json(
···
try {
const user = requireAuth(req);
-
const rateLimitError = enforceRateLimit(req, "passkey-register-options", {
-
ip: { max: 10, windowSeconds: 5 * 60 },
-
});
+
const rateLimitError = enforceRateLimit(
+
req,
+
"passkey-register-options",
+
{
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
},
+
);
if (rateLimitError) return rateLimitError;
const options = await createRegistrationOptions(user);
···
try {
const _user = requireAuth(req);
-
const rateLimitError = enforceRateLimit(req, "passkey-register-verify", {
-
ip: { max: 10, windowSeconds: 5 * 60 },
-
});
+
const rateLimitError = enforceRateLimit(
+
req,
+
"passkey-register-verify",
+
{
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
},
+
);
if (rateLimitError) return rateLimitError;
const body = await req.json();
···
const { encodeCursor } = await import("./lib/cursor");
const last = transcriptions[transcriptions.length - 1];
if (last) {
-
nextCursor = encodeCursor([
-
last.created_at.toString(),
-
last.id,
-
]);
+
nextCursor = encodeCursor([last.created_at.toString(), last.id]);
···
server.stop();
// 2. Close all active SSE streams (safe to kill - sync will handle reconnection)
-
console.log(`[Shutdown] Closing ${activeSSEStreams.size} active SSE streams...`);
+
console.log(
+
`[Shutdown] Closing ${activeSSEStreams.size} active SSE streams...`,
+
);
for (const controller of activeSSEStreams) {
try {
controller.close();
+1 -1
src/lib/api-response-format.test.ts
···
/**
* API Response Format Standards
-
*
+
*
* This test documents the standardized response formats across the API.
* All endpoints should follow these patterns for consistency.
*/
+4 -1
src/lib/auth.ts
···
const { encodeCursor } = require("./cursor");
const last = users[users.length - 1];
if (last) {
-
nextCursor = encodeCursor([last.created_at.toString(), last.id.toString()]);
+
nextCursor = encodeCursor([
+
last.created_at.toString(),
+
last.id.toString(),
+
]);
}
}
+4 -1
src/lib/classes.ts
···
const { year, semester, courseCode, id } = decodeClassCursor(cursor);
classes = db
-
.query<ClassWithStats, [number, number, string, string, string, number]>(
+
.query<
+
ClassWithStats,
+
[number, number, string, string, string, number]
+
>(
`SELECT c.* FROM classes c
INNER JOIN class_members cm ON c.id = cm.class_id
WHERE cm.user_id = ? AND
+3 -3
src/lib/cursor.test.ts
···
import { describe, expect, test } from "bun:test";
import {
-
encodeCursor,
+
decodeClassCursor,
decodeCursor,
-
encodeSimpleCursor,
decodeSimpleCursor,
encodeClassCursor,
-
decodeClassCursor,
+
encodeCursor,
+
encodeSimpleCursor,
} from "./cursor";
describe("Cursor encoding/decoding", () => {
+3 -13
src/lib/pagination.test.ts
···
-
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
import { Database } from "bun:sqlite";
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
let testDb: Database;
···
// Create test users
testDb.run(
"INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)",
-
[
-
"user1@test.com",
-
"hash1",
-
Math.floor(Date.now() / 1000) - 100,
-
"user",
-
],
+
["user1@test.com", "hash1", Math.floor(Date.now() / 1000) - 100, "user"],
);
testDb.run(
"INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)",
-
[
-
"user2@test.com",
-
"hash2",
-
Math.floor(Date.now() / 1000) - 50,
-
"user",
-
],
+
["user2@test.com", "hash2", Math.floor(Date.now() / 1000) - 50, "user"],
);
testDb.run(
"INSERT INTO users (email, password_hash, email_verified, created_at, role) VALUES (?, ?, 1, ?, ?)",
+104 -89
src/pages/admin.ts
···
-
const transcriptionsComponent = document.getElementById('transcriptions-component') as any;
-
const usersComponent = document.getElementById('users-component') as any;
-
const userModal = document.getElementById('user-modal') as any;
-
const transcriptModal = document.getElementById('transcript-modal') as any;
-
const errorMessage = document.getElementById('error-message') as HTMLElement;
-
const loading = document.getElementById('loading') as HTMLElement;
-
const content = document.getElementById('content') as HTMLElement;
+
const transcriptionsComponent = document.getElementById(
+
"transcriptions-component",
+
) as HTMLElement | null;
+
const usersComponent = document.getElementById(
+
"users-component",
+
) as HTMLElement | null;
+
const userModal = document.getElementById("user-modal") as HTMLElement | null;
+
const transcriptModal = document.getElementById(
+
"transcript-modal",
+
) as HTMLElement | null;
+
const errorMessage = document.getElementById("error-message") as HTMLElement;
+
const loading = document.getElementById("loading") as HTMLElement;
+
const content = document.getElementById("content") as HTMLElement;
// Modal functions
function openUserModal(userId: string) {
-
userModal.setAttribute('open', '');
-
userModal.userId = userId;
+
userModal.setAttribute("open", "");
+
userModal.userId = userId;
}
function closeUserModal() {
-
userModal.removeAttribute('open');
-
userModal.userId = null;
+
userModal.removeAttribute("open");
+
userModal.userId = null;
}
function openTranscriptModal(transcriptId: string) {
-
transcriptModal.setAttribute('open', '');
-
transcriptModal.transcriptId = transcriptId;
+
transcriptModal.setAttribute("open", "");
+
transcriptModal.transcriptId = transcriptId;
}
function closeTranscriptModal() {
-
transcriptModal.removeAttribute('open');
-
transcriptModal.transcriptId = null;
+
transcriptModal.removeAttribute("open");
+
transcriptModal.transcriptId = null;
}
// Listen for component events
-
transcriptionsComponent?.addEventListener('open-transcription', (e: CustomEvent) => {
-
openTranscriptModal(e.detail.id);
-
});
+
transcriptionsComponent?.addEventListener(
+
"open-transcription",
+
(e: CustomEvent) => {
+
openTranscriptModal(e.detail.id);
+
},
+
);
-
usersComponent?.addEventListener('open-user', (e: CustomEvent) => {
-
openUserModal(e.detail.id);
+
usersComponent?.addEventListener("open-user", (e: CustomEvent) => {
+
openUserModal(e.detail.id);
});
// Listen for modal close events
-
userModal?.addEventListener('close', closeUserModal);
-
userModal?.addEventListener('user-updated', async () => {
-
await loadStats();
+
userModal?.addEventListener("close", closeUserModal);
+
userModal?.addEventListener("user-updated", async () => {
+
await loadStats();
});
-
userModal?.addEventListener('click', (e: MouseEvent) => {
-
if (e.target === userModal) closeUserModal();
+
userModal?.addEventListener("click", (e: MouseEvent) => {
+
if (e.target === userModal) closeUserModal();
});
-
transcriptModal?.addEventListener('close', closeTranscriptModal);
-
transcriptModal?.addEventListener('transcript-deleted', async () => {
-
await loadStats();
+
transcriptModal?.addEventListener("close", closeTranscriptModal);
+
transcriptModal?.addEventListener("transcript-deleted", async () => {
+
await loadStats();
});
-
transcriptModal?.addEventListener('click', (e: MouseEvent) => {
-
if (e.target === transcriptModal) closeTranscriptModal();
+
transcriptModal?.addEventListener("click", (e: MouseEvent) => {
+
if (e.target === transcriptModal) closeTranscriptModal();
});
async function loadStats() {
-
try {
-
const [transcriptionsRes, usersRes] = await Promise.all([
-
fetch('/api/admin/transcriptions'),
-
fetch('/api/admin/users')
-
]);
+
try {
+
const [transcriptionsRes, usersRes] = await Promise.all([
+
fetch("/api/admin/transcriptions"),
+
fetch("/api/admin/users"),
+
]);
-
if (!transcriptionsRes.ok || !usersRes.ok) {
-
if (transcriptionsRes.status === 403 || usersRes.status === 403) {
-
window.location.href = '/';
-
return;
-
}
-
throw new Error('Failed to load admin data');
-
}
+
if (!transcriptionsRes.ok || !usersRes.ok) {
+
if (transcriptionsRes.status === 403 || usersRes.status === 403) {
+
window.location.href = "/";
+
return;
+
}
+
throw new Error("Failed to load admin data");
+
}
+
+
const transcriptions = await transcriptionsRes.json();
+
const users = await usersRes.json();
+
+
const totalUsers = document.getElementById("total-users");
+
const totalTranscriptions = document.getElementById("total-transcriptions");
+
const failedTranscriptions = document.getElementById(
+
"failed-transcriptions",
+
);
-
const transcriptions = await transcriptionsRes.json();
-
const users = await usersRes.json();
+
if (totalUsers) totalUsers.textContent = users.length.toString();
+
if (totalTranscriptions)
+
totalTranscriptions.textContent = transcriptions.length.toString();
-
const totalUsers = document.getElementById('total-users');
-
const totalTranscriptions = document.getElementById('total-transcriptions');
-
const failedTranscriptions = document.getElementById('failed-transcriptions');
-
-
if (totalUsers) totalUsers.textContent = users.length.toString();
-
if (totalTranscriptions) totalTranscriptions.textContent = transcriptions.length.toString();
-
-
const failed = transcriptions.filter((t: any) => t.status === 'failed');
-
if (failedTranscriptions) failedTranscriptions.textContent = failed.length.toString();
+
const failed = transcriptions.filter(
+
(t: { status: string }) => t.status === "failed",
+
);
+
if (failedTranscriptions)
+
failedTranscriptions.textContent = failed.length.toString();
-
loading.classList.add('hidden');
-
content.classList.remove('hidden');
-
} catch (error) {
-
errorMessage.textContent = (error as Error).message;
-
errorMessage.classList.remove('hidden');
-
loading.classList.add('hidden');
-
}
+
loading.classList.add("hidden");
+
content.classList.remove("hidden");
+
} catch (error) {
+
errorMessage.textContent = (error as Error).message;
+
errorMessage.classList.remove("hidden");
+
loading.classList.add("hidden");
+
}
}
// Tab switching
function switchTab(tabName: string) {
-
document.querySelectorAll('.tab').forEach(t => {
-
t.classList.remove('active');
-
});
-
document.querySelectorAll('.tab-content').forEach(c => {
-
c.classList.remove('active');
-
});
+
document.querySelectorAll(".tab").forEach((t) => {
+
t.classList.remove("active");
+
});
+
document.querySelectorAll(".tab-content").forEach((c) => {
+
c.classList.remove("active");
+
});
-
const tabButton = document.querySelector(`[data-tab="${tabName}"]`);
-
const tabContent = document.getElementById(`${tabName}-tab`);
-
-
if (tabButton && tabContent) {
-
tabButton.classList.add('active');
-
tabContent.classList.add('active');
-
-
// Update URL without reloading
-
const url = new URL(window.location.href);
-
url.searchParams.set('tab', tabName);
-
// Remove subtab param when leaving classes tab
-
if (tabName !== 'classes') {
-
url.searchParams.delete('subtab');
-
}
-
window.history.pushState({}, '', url);
-
}
+
const tabButton = document.querySelector(`[data-tab="${tabName}"]`);
+
const tabContent = document.getElementById(`${tabName}-tab`);
+
+
if (tabButton && tabContent) {
+
tabButton.classList.add("active");
+
tabContent.classList.add("active");
+
+
// Update URL without reloading
+
const url = new URL(window.location.href);
+
url.searchParams.set("tab", tabName);
+
// Remove subtab param when leaving classes tab
+
if (tabName !== "classes") {
+
url.searchParams.delete("subtab");
+
}
+
window.history.pushState({}, "", url);
+
}
}
-
document.querySelectorAll('.tab').forEach(tab => {
-
tab.addEventListener('click', () => {
-
switchTab((tab as HTMLElement).dataset.tab || '');
-
});
+
document.querySelectorAll(".tab").forEach((tab) => {
+
tab.addEventListener("click", () => {
+
switchTab((tab as HTMLElement).dataset.tab || "");
+
});
});
// Check for tab query parameter on load
const params = new URLSearchParams(window.location.search);
-
const initialTab = params.get('tab');
-
const validTabs = ['pending', 'transcriptions', 'users', 'classes'];
+
const initialTab = params.get("tab");
+
const validTabs = ["pending", "transcriptions", "users", "classes"];
if (initialTab && validTabs.includes(initialTab)) {
-
switchTab(initialTab);
+
switchTab(initialTab);
}
// Initialize
+12 -8
src/pages/index.ts
···
-
document.getElementById('start-btn')?.addEventListener('click', async () => {
-
const authComponent = document.querySelector('auth-component') as any;
-
const isLoggedIn = await authComponent.isAuthenticated();
+
document.getElementById("start-btn")?.addEventListener("click", async () => {
+
const authComponent = document.querySelector("auth-component");
+
if (!authComponent) return;
-
if (isLoggedIn) {
-
window.location.href = '/classes';
-
} else {
-
authComponent.openAuthModal();
-
}
+
const isLoggedIn = await (
+
authComponent as { isAuthenticated: () => Promise<boolean> }
+
).isAuthenticated();
+
+
if (isLoggedIn) {
+
window.location.href = "/classes";
+
} else {
+
(authComponent as { openAuthModal: () => void }).openAuthModal();
+
}
});
+4 -4
src/pages/reset-password.ts
···
// Wait for component to be defined before setting token
-
await customElements.whenDefined('reset-password-form');
+
await customElements.whenDefined("reset-password-form");
// Get token from URL and pass to component
const urlParams = new URLSearchParams(window.location.search);
-
const token = urlParams.get('token');
-
const resetForm = document.getElementById('reset-form') as any;
+
const token = urlParams.get("token");
+
const resetForm = document.getElementById("reset-form");
if (resetForm) {
-
resetForm.token = token;
+
(resetForm as { token: string | null }).token = token;
}
+64 -64
src/styles/admin.css
···
main {
-
max-width: 80rem;
-
margin: 0 auto;
-
padding: 2rem;
+
max-width: 80rem;
+
margin: 0 auto;
+
padding: 2rem;
}
h1 {
-
margin-bottom: 2rem;
-
color: var(--text);
+
margin-bottom: 2rem;
+
color: var(--text);
}
.section {
-
margin-bottom: 3rem;
+
margin-bottom: 3rem;
}
.section-title {
-
font-size: 1.5rem;
-
font-weight: 600;
-
color: var(--text);
-
margin-bottom: 1rem;
-
display: flex;
-
align-items: center;
-
gap: 0.5rem;
+
font-size: 1.5rem;
+
font-weight: 600;
+
color: var(--text);
+
margin-bottom: 1rem;
+
display: flex;
+
align-items: center;
+
gap: 0.5rem;
}
.tabs {
-
display: flex;
-
gap: 1rem;
-
border-bottom: 2px solid var(--secondary);
-
margin-bottom: 2rem;
+
display: flex;
+
gap: 1rem;
+
border-bottom: 2px solid var(--secondary);
+
margin-bottom: 2rem;
}
.tab {
-
padding: 0.75rem 1.5rem;
-
border: none;
-
background: transparent;
-
color: var(--text);
-
cursor: pointer;
-
font-size: 1rem;
-
font-weight: 500;
-
font-family: inherit;
-
border-bottom: 2px solid transparent;
-
margin-bottom: -2px;
-
transition: all 0.2s;
+
padding: 0.75rem 1.5rem;
+
border: none;
+
background: transparent;
+
color: var(--text);
+
cursor: pointer;
+
font-size: 1rem;
+
font-weight: 500;
+
font-family: inherit;
+
border-bottom: 2px solid transparent;
+
margin-bottom: -2px;
+
transition: all 0.2s;
}
.tab:hover {
-
color: var(--primary);
+
color: var(--primary);
}
.tab.active {
-
color: var(--primary);
-
border-bottom-color: var(--primary);
+
color: var(--primary);
+
border-bottom-color: var(--primary);
}
.tab-content {
-
display: none;
+
display: none;
}
.tab-content.active {
-
display: block;
+
display: block;
}
.empty-state {
-
text-align: center;
-
padding: 3rem;
-
color: var(--text);
-
opacity: 0.6;
+
text-align: center;
+
padding: 3rem;
+
color: var(--text);
+
opacity: 0.6;
}
.loading {
-
text-align: center;
-
padding: 3rem;
-
color: var(--text);
+
text-align: center;
+
padding: 3rem;
+
color: var(--text);
}
.error {
-
background: #fee2e2;
-
color: #991b1b;
-
padding: 1rem;
-
border-radius: 6px;
-
margin-bottom: 1rem;
+
background: #fee2e2;
+
color: #991b1b;
+
padding: 1rem;
+
border-radius: 6px;
+
margin-bottom: 1rem;
}
.stats {
-
display: grid;
-
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
-
gap: 1rem;
-
margin-bottom: 2rem;
+
display: grid;
+
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
+
gap: 1rem;
+
margin-bottom: 2rem;
}
.stat-card {
-
background: var(--background);
-
border: 2px solid var(--secondary);
-
border-radius: 8px;
-
padding: 1.5rem;
+
background: var(--background);
+
border: 2px solid var(--secondary);
+
border-radius: 8px;
+
padding: 1.5rem;
}
.stat-value {
-
font-size: 2rem;
-
font-weight: 700;
-
color: var(--primary);
-
margin-bottom: 0.25rem;
+
font-size: 2rem;
+
font-weight: 700;
+
color: var(--primary);
+
margin-bottom: 0.25rem;
}
.stat-label {
-
color: var(--text);
-
opacity: 0.7;
-
font-size: 0.875rem;
+
color: var(--text);
+
opacity: 0.7;
+
font-size: 0.875rem;
}
.timestamp {
-
color: var(--text);
-
opacity: 0.6;
-
font-size: 0.875rem;
+
color: var(--text);
+
opacity: 0.6;
+
font-size: 0.875rem;
}
.hidden {
-
display: none;
+
display: none;
}
+41 -41
src/styles/index.css
···
.hero-title {
-
font-size: 3rem;
-
font-weight: 700;
-
color: var(--text);
-
margin-bottom: 1rem;
+
font-size: 3rem;
+
font-weight: 700;
+
color: var(--text);
+
margin-bottom: 1rem;
}
.hero-subtitle {
-
font-size: 1.25rem;
-
color: var(--text);
-
opacity: 0.8;
-
margin-bottom: 2rem;
+
font-size: 1.25rem;
+
color: var(--text);
+
opacity: 0.8;
+
margin-bottom: 2rem;
}
main {
-
text-align: center;
-
padding: 4rem 2rem;
+
text-align: center;
+
padding: 4rem 2rem;
}
.cta-buttons {
-
display: flex;
-
gap: 1rem;
-
justify-content: center;
-
margin-top: 2rem;
+
display: flex;
+
gap: 1rem;
+
justify-content: center;
+
margin-top: 2rem;
}
.btn {
-
padding: 0.75rem 1.5rem;
-
border-radius: 6px;
-
font-size: 1rem;
-
font-weight: 500;
-
cursor: pointer;
-
transition: all 0.2s;
-
font-family: inherit;
-
border: 2px solid;
-
text-decoration: none;
-
display: inline-block;
+
padding: 0.75rem 1.5rem;
+
border-radius: 6px;
+
font-size: 1rem;
+
font-weight: 500;
+
cursor: pointer;
+
transition: all 0.2s;
+
font-family: inherit;
+
border: 2px solid;
+
text-decoration: none;
+
display: inline-block;
}
.btn-primary {
-
background: var(--primary);
-
color: white;
-
border-color: var(--primary);
+
background: var(--primary);
+
color: white;
+
border-color: var(--primary);
}
.btn-primary:hover {
-
background: transparent;
-
color: var(--primary);
+
background: transparent;
+
color: var(--primary);
}
.btn-secondary {
-
background: transparent;
-
color: var(--text);
-
border-color: var(--secondary);
+
background: transparent;
+
color: var(--text);
+
border-color: var(--secondary);
}
.btn-secondary:hover {
-
border-color: var(--primary);
-
color: var(--primary);
+
border-color: var(--primary);
+
color: var(--primary);
}
@media (max-width: 640px) {
-
.hero-title {
-
font-size: 2.5rem;
-
}
+
.hero-title {
+
font-size: 2.5rem;
+
}
-
.cta-buttons {
-
flex-direction: column;
-
align-items: center;
-
}
+
.cta-buttons {
+
flex-direction: column;
+
align-items: center;
+
}
}
+4 -4
src/styles/reset-password.css
···
main {
-
display: flex;
-
align-items: center;
-
justify-content: center;
-
padding: 4rem 1rem;
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
padding: 4rem 1rem;
}
+1 -1
src/styles/settings.css
···
main {
-
max-width: 64rem;
+
max-width: 64rem;
}
+13 -13
src/styles/transcribe.css
···
.page-header {
-
text-align: center;
-
margin-bottom: 3rem;
+
text-align: center;
+
margin-bottom: 3rem;
}
.page-title {
-
font-size: 2.5rem;
-
font-weight: 700;
-
color: var(--text);
-
margin-bottom: 0.5rem;
+
font-size: 2.5rem;
+
font-weight: 700;
+
color: var(--text);
+
margin-bottom: 0.5rem;
}
.page-subtitle {
-
font-size: 1.125rem;
-
color: var(--text);
-
opacity: 0.8;
+
font-size: 1.125rem;
+
color: var(--text);
+
opacity: 0.8;
}
.back-link {
-
color: var(--paynes-gray);
-
text-decoration: none;
-
font-size: 0.875rem;
+
color: var(--paynes-gray);
+
text-decoration: none;
+
font-size: 0.875rem;
}
.mb-1 {
-
margin-bottom: 1rem;
+
margin-bottom: 1rem;
}