🪻 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