🪻 distributed transcription service thistle.dunkirk.sh
1# Thistle - Project Guidelines 2 3This is a Bun-based transcription service using the [Bun fullstack pattern](https://bun.com/docs/bundler/fullstack) for routing and bundled HTML. 4 5## Project Info 6 7- Name: Thistle 8- Purpose: Transcription service 9- Runtime: Bun (NOT Node.js) 10- Language: TypeScript with strict mode 11- Frontend: Vanilla HTML/CSS/JS with lightweight helpers on top of web components 12 13## Design System 14 15ALWAYS use the project's CSS variables for colors: 16 17```css 18:root { 19 /* Color palette */ 20 --gunmetal: #2d3142ff; /* dark blue-gray */ 21 --paynes-gray: #4f5d75ff; /* medium blue-gray */ 22 --silver: #bfc0c0ff; /* light gray */ 23 --white: #ffffffff; /* white */ 24 --coral: #ef8354ff; /* warm orange */ 25 26 /* Semantic color assignments */ 27 --text: var(--gunmetal); 28 --background: var(--white); 29 --primary: var(--paynes-gray); 30 --secondary: var(--silver); 31 --accent: var(--coral); 32} 33``` 34 35**Color usage:** 36- NEVER hardcode colors like `#4f46e5`, `white`, `red`, etc. 37- Always use semantic variables (`var(--primary)`, `var(--background)`, `var(--accent)`, etc.) or named color variables (`var(--gunmetal)`, `var(--coral)`, etc.) 38 39**Dimensions:** 40- Use `rem` for all sizes, spacing, and widths (not `px`) 41- Base font size is 16px (1rem = 16px) 42- Common values: `0.5rem` (8px), `1rem` (16px), `2rem` (32px), `3rem` (48px) 43- Max widths: `48rem` (768px) for content, `56rem` (896px) for forms/data 44- Spacing scale: `0.25rem`, `0.5rem`, `0.75rem`, `1rem`, `1.5rem`, `2rem`, `3rem` 45 46## NO FRAMEWORKS 47 48NEVER use React, Vue, Svelte, or any heavy framework. 49 50This project prioritizes: 51- Speed: Minimal JavaScript, fast load times 52- Small bundle sizes: Keep bundles tiny 53- Native web platform: Use web standards (Web Components, native DOM APIs) 54- Simplicity: Vanilla HTML, CSS, and JavaScript 55 56Allowed lightweight helpers: 57- Lit (~8-10KB gzipped) for reactive web components 58- Native Web Components 59- Plain JavaScript/TypeScript 60 61Explicitly forbidden: 62- React, React DOM 63- Vue 64- Svelte 65- Angular 66- Any framework with a virtual DOM or large runtime 67 68## Commands 69 70```bash 71# Install dependencies 72bun install 73 74# Development server with hot reload 75bun dev 76 77# Run tests 78bun test 79 80# Build files 81bun build <file.html|file.ts|file.css> 82 83# Make a user an admin 84bun scripts/make-admin.ts <email> 85``` 86 87Development workflow: `bun dev` runs the server with hot module reloading. Changes to TypeScript, HTML, or CSS files automatically reload. 88 89**IMPORTANT**: NEVER run `bun dev` yourself - the user always has it running already. 90 91## Bun Usage 92 93Default to using Bun instead of Node.js. 94 95- Use `bun <file>` instead of `node <file>` or `ts-node <file>` 96- Use `bun test` instead of `jest` or `vitest` 97- Use `bun build <file>` instead of `webpack` or `esbuild` 98- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` 99- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` 100- Bun automatically loads .env, so don't use dotenv 101 102## Bun APIs 103 104Use Bun's built-in APIs instead of npm packages: 105 106- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`. 107- `bun:sqlite` for SQLite. Don't use `better-sqlite3`. 108- `Bun.redis` for Redis. Don't use `ioredis`. 109- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`. 110- `WebSocket` is built-in. Don't use `ws`. 111- Prefer `Bun.file` over `node:fs`'s readFile/writeFile 112- `Bun.$\`ls\`` instead of execa 113 114## Server Setup 115 116Use `Bun.serve()` with the routes pattern: 117 118```ts 119import index from "./index.html" 120 121Bun.serve({ 122 routes: { 123 "/": index, 124 "/api/users/:id": { 125 GET: (req) => { 126 return new Response(JSON.stringify({ id: req.params.id })); 127 }, 128 }, 129 }, 130 // optional websocket support 131 websocket: { 132 open: (ws) => { 133 ws.send("Hello, world!"); 134 }, 135 message: (ws, message) => { 136 ws.send(message); 137 }, 138 close: (ws) => { 139 // handle close 140 } 141 }, 142 development: { 143 hmr: true, 144 console: true, 145 } 146}) 147``` 148 149## Frontend Pattern 150 151Don't use Vite or any build tools. Use HTML imports with `Bun.serve()`. 152 153HTML files can directly import `.ts` or `.js` files: 154 155```html 156<!DOCTYPE html> 157<html lang="en"> 158 159<head> 160 <meta charset="UTF-8"> 161 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 162 <title>Page Title - Thistle</title> 163 <link rel="icon" 164 href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>🪻</text></svg>"> 165 <link rel="stylesheet" href="../styles/main.css"> 166</head> 167 168<body> 169 <auth-component></auth-component> 170 171 <main> 172 <h1>Page Title</h1> 173 <my-component></my-component> 174 </main> 175 176 <script type="module" src="../components/auth.ts"></script> 177 <script type="module" src="../components/my-component.ts"></script> 178</body> 179 180</html> 181``` 182 183**Standard HTML template:** 184- Always include the `<auth-component>` element for consistent login/logout UI 185- Always include the thistle emoji favicon 186- Always include proper meta tags (charset, viewport) 187- Structure: auth component, then main content, then scripts 188- Import `auth.ts` on every page for authentication UI 189 190Bun's bundler will transpile and bundle automatically. `<link>` tags pointing to stylesheets work with Bun's CSS bundler. 191 192Frontend TypeScript (vanilla or with Lit web components): 193 194```ts 195import { LitElement, html, css } from 'lit'; 196import { customElement, property } from 'lit/decorators.js'; 197 198// Define a Lit web component 199@customElement('my-component') 200export class MyComponent extends LitElement { 201 @property({ type: String }) name = 'World'; 202 203 // Scoped styles using css tagged template 204 static styles = css` 205 :host { 206 display: block; 207 padding: 1rem; 208 } 209 .greeting { 210 color: blue; 211 } 212 `; 213 214 // Render using html tagged template 215 render() { 216 return html` 217 <div class="greeting"> 218 Hello, ${this.name}! 219 </div> 220 `; 221 } 222} 223 224// Or use plain DOM manipulation for simple interactions 225document.querySelector('h1')?.addEventListener('click', () => { 226 console.log('Clicked!'); 227}); 228``` 229 230**When to use Lit:** 231- Components with reactive properties (auto-updates when data changes) 232- Complex components needing scoped styles 233- Form controls with internal state 234- Components with lifecycle needs 235 236**When to skip Lit:** 237- Static content (use plain HTML) 238- Simple one-off interactions (use vanilla JS) 239- Anything without reactive state 240 241Lit provides: 242- `@customElement` decorator to register components 243- `@property` decorator for reactive properties 244- `html` tagged template for declarative rendering 245- `css` tagged template for scoped styles 246- Automatic re-rendering when properties change 247- Size: ~8-10KB minified+gzipped 248 249## Testing 250 251Use `bun test` to run tests. 252 253### Basic Test Structure 254 255```ts 256import { test, expect } from "bun:test"; 257 258test("hello world", () => { 259 expect(1).toBe(1); 260}); 261``` 262 263### Test File Naming 264 265- Place tests next to the code they test: `foo.ts``foo.test.ts` 266- This keeps tests close to implementation for easy maintenance 267- Bun automatically discovers `*.test.ts` files 268 269### Writing Good Tests 270 271**Test security-critical code:** 272- File path operations (directory traversal, injection) 273- User input validation 274- Authentication/authorization 275- API endpoint security 276 277**Test edge cases:** 278- Empty strings, null, undefined 279- Very large inputs (size limits) 280- Invalid formats 281- Boundary conditions 282 283**Test async operations:** 284```ts 285test("async function", async () => { 286 const result = await someAsyncFunction(); 287 expect(result).toBe("expected value"); 288}); 289``` 290 291**Test error conditions:** 292```ts 293test("rejects invalid input", async () => { 294 await expect(dangerousFunction("../../../etc/passwd")).rejects.toThrow(); 295 await expect(dangerousFunction("invalid")).rejects.toThrow("Invalid format"); 296}); 297``` 298 299**Example: Security-focused tests** 300```ts 301test("prevents directory traversal", async () => { 302 const maliciousIds = [ 303 "../../../etc/passwd", 304 "../../secret.txt", 305 "test/../../../config", 306 ]; 307 308 for (const id of maliciousIds) { 309 await expect(loadFile(id)).rejects.toThrow(); 310 } 311}); 312 313test("validates input format", async () => { 314 const invalidInputs = [ 315 "test; rm -rf /", 316 "test`whoami`", 317 "test\x00null", 318 ]; 319 320 for (const input of invalidInputs) { 321 await expect(processInput(input)).rejects.toThrow("Invalid format"); 322 } 323}); 324``` 325 326### Running Tests 327 328```bash 329# Run all tests 330bun test 331 332# Run specific test file 333bun test src/lib/auth.test.ts 334 335# Watch mode (re-run on changes) 336bun test --watch 337``` 338 339### What to Test 340 341**Always test:** 342- Security-critical functions (file I/O, user input) 343- Complex business logic 344- Edge cases and error handling 345- Public API functions 346 347**Don't need to test:** 348- Simple getters/setters 349- Framework/library code 350- UI components (unless complex logic) 351- One-line utility functions 352 353## TypeScript Configuration 354 355Strict mode is enabled with these settings: 356 357```json 358{ 359 "strict": true, 360 "noFallthroughCasesInSwitch": true, 361 "noUncheckedIndexedAccess": true, 362 "noImplicitOverride": true 363} 364``` 365 366Deliberately disabled: 367- `noUnusedLocals`: false 368- `noUnusedParameters`: false 369- `noPropertyAccessFromIndexSignature`: false 370 371Module system: 372- `moduleResolution`: "bundler" 373- `module`: "Preserve" 374- JSX: `preserve` (NOT react-jsx - we don't use React) 375- Allows importing `.ts` extensions directly 376 377## Frontend Technologies 378 379Core (always use): 380- Vanilla HTML, CSS, JavaScript/TypeScript 381- Native Web Components API 382- Native DOM APIs (querySelector, addEventListener, etc.) 383 384Lightweight helpers: 385- Lit (~8-10KB gzipped): For reactive web components with state management 386 387Bundle size philosophy: 388- Start with vanilla JS 389- Add helpers only when they significantly reduce complexity 390- Measure bundle size impact before adding any library 391- Target: Keep total JS bundle under 50KB 392 393## Project Structure 394 395Based on Bun fullstack pattern: 396- `src/index.ts`: Server imports HTML files as modules 397- `src/pages/`: HTML files (route entry points) 398- `src/components/`: Lit web components 399- `src/styles/`: CSS files 400- `public/`: Static assets (images, fonts, etc.) 401 402**File flow:** 4031. Server imports HTML: `import indexHTML from "./pages/index.html"` 4042. HTML imports components: `<script type="module" src="../components/counter.ts"></script>` 4053. HTML links styles: `<link rel="stylesheet" href="../styles/main.css">` 4064. Components self-register as custom elements 4075. Bun bundles everything automatically 408 409## Database Schema & Migrations 410 411Database migrations are managed in `src/db/schema.ts` using a versioned migration system. 412 413**Migration structure:** 414```typescript 415const migrations = [ 416 { 417 version: 1, 418 name: "Description of migration", 419 sql: ` 420 CREATE TABLE IF NOT EXISTS ...; 421 CREATE INDEX IF NOT EXISTS ...; 422 `, 423 }, 424]; 425``` 426 427**Important migration rules:** 4281. **Never modify existing migrations** - they may have already run in production 4292. **Always add new migrations** with incrementing version numbers 4303. **Drop indexes before dropping columns** - SQLite will error if you try to drop a column with an index still attached 4314. **Use IF NOT EXISTS** for CREATE statements to be idempotent 4325. **Test migrations** on a copy of production data before deploying 433 434**Example: Dropping a column** 435```sql 436-- ❌ WRONG: Will error if idx_users_old_column exists 437ALTER TABLE users DROP COLUMN old_column; 438 439-- ✅ CORRECT: Drop index first, then column 440DROP INDEX IF EXISTS idx_users_old_column; 441ALTER TABLE users DROP COLUMN old_column; 442``` 443 444**Migration workflow:** 4451. Add migration to `migrations` array with next version number 4462. Migrations auto-apply on server start 4473. Check `schema_migrations` table to see applied versions 4484. Migrations are transactional and show timing in console 449 450## File Organization 451 452- `src/index.ts`: Main server entry point with `Bun.serve()` routes 453- `src/pages/*.html`: Route entry points (imported as modules) 454- `src/components/*.ts`: Lit web components 455- `src/styles/*.css`: Stylesheets (linked from HTML) 456- `public/`: Static assets directory 457- Tests: `*.test.ts` files 458 459**Current structure example:** 460``` 461src/ 462 index.ts # Imports HTML, defines routes 463 pages/ 464 index.html # Imports components via <script type="module"> 465 components/ 466 counter.ts # Lit component with @customElement 467 styles/ 468 main.css # Linked from HTML with <link> 469``` 470 471## Naming Conventions 472 473Follow TypeScript conventions: 474- PascalCase for components and classes 475- camelCase for functions and variables 476- kebab-case for file names 477 478## Development Workflow 479 4801. Make changes to `.ts`, `.html`, or `.css` files 4812. Bun's HMR automatically reloads changes 4823. Write tests in `*.test.ts` files 4834. Run `bun test` to verify 484 485## IDE Setup 486 487Biome LSP is configured in `crush.json` for linting and formatting support. 488 489## Common Tasks 490 491### Adding a new route 492Add to the `routes` object in `Bun.serve()` configuration 493 494### Adding a new page 495Create an HTML file, import it in the server, add to routes 496 497### Adding frontend functionality 498Import TS/JS files directly from HTML using `<script type="module" src="../components/my-component.ts"></script>`. Use Lit for reactive components or vanilla JS for simple interactions. Never React. 499 500### Adding WebSocket support 501Add `websocket` configuration to `Bun.serve()` 502 503## Important Notes 504 5051. No npm scripts needed: Bun is fast enough to run commands directly 5062. Private package: `package.json` has `"private": true` 5073. No build step for development: Hot reload handles everything 5084. Module type: Package uses `"type": "module"` (ESM) 5095. Bun types: Available via `@types/bun` (check `node_modules/bun-types/docs/**.md` for API docs) 510 511## Gotchas 512 5131. Don't use Node.js commands: Use `bun` instead of `node`, `npm`, `npx`, etc. 5142. Don't install Express/Vite/other tools: Bun has built-in equivalents 5153. NEVER EVER use React: This project is vanilla JS/TS with web components only. React is explicitly forbidden. 5164. Import .ts extensions: Bun allows importing `.ts` files directly 5175. No dotenv needed: Bun loads `.env` automatically 5186. HTML imports are special: They trigger Bun's bundler, don't treat them as static files 5197. Bundle size matters: Always consider the size impact before adding any library 520 521## Documentation Lookup 522 523Use Context7 MCP for looking up official documentation for libraries and frameworks. 524 525## Resources 526 527- [Bun Fullstack Documentation](https://bun.com/docs/bundler/fullstack) 528- [Lit Documentation](https://lit.dev/) 529- [Web Components MDN](https://developer.mozilla.org/en-US/docs/Web/Web_Components) 530- Bun API docs in `node_modules/bun-types/docs/**.md` 531 532## Admin System 533 534The application includes a role-based admin system for managing users and transcriptions. 535 536**User roles:** 537- `user` - Default role, can create and manage their own transcriptions 538- `admin` - Full administrative access to all data and users 539 540**Admin privileges:** 541- View all transcriptions (with user info, status, errors) 542- Delete transcriptions 543- View all users (with emails, join dates, roles) 544- Change user roles (user ↔ admin) 545- Delete user accounts 546- Access admin dashboard at `/admin` 547 548**Making users admin:** 549Use the provided script to grant admin access: 550```bash 551bun scripts/make-admin.ts user@example.com 552``` 553 554**Admin routes:** 555- `/admin` - Admin dashboard (protected by `requireAdmin` middleware) 556- `/api/admin/transcriptions` - Get all transcriptions with user info 557- `/api/admin/transcriptions/:id` - Delete a transcription (DELETE) 558- `/api/admin/users` - Get all users 559- `/api/admin/users/:id` - Delete a user account (DELETE) 560- `/api/admin/users/:id/role` - Update a user's role (PUT) 561 562**Admin UI features:** 563- Statistics cards (total users, total/failed transcriptions) 564- Tabbed interface (Transcriptions / Users) 565- Status badges for transcription states 566- Delete buttons for transcriptions with confirmation 567- Role dropdown for changing user roles 568- Delete buttons for user accounts with confirmation 569- User avatars and info display 570- Timestamp formatting 571- Admin badge on user listings 572 573**Implementation notes:** 574- `role` column in users table ('user' or 'admin', default 'user') 575- `requireAdmin()` middleware checks authentication + admin role 576- Returns 403 if non-admin tries to access admin routes 577- Admin link shows in auth menu only for admin users 578- Redirects to home page if non-admin accesses admin page 579 580## Future Additions 581 582As the codebase grows, document: 583- Database schema and migrations 584- API endpoint patterns 585- Authentication/authorization approach 586- Transcription service integration details 587- Deployment process 588- Environment variables needed 589