Replace auto-generated CLAUDE.md with proper project documentation
- Architecture overview (backend + frontend) - Stack details (Bun/Hono/Drizzle + Next.js/shadcn) - How to run locally (backend, frontend, migrations) - API endpoint reference - Key design decisions (sequential IDs, transaction-centric, scrip engine) - Git workflow and common issues
This commit is contained in:
184
CLAUDE.md
184
CLAUDE.md
@@ -1,106 +1,114 @@
|
|||||||
|
# Tessera
|
||||||
|
|
||||||
Default to using Bun instead of Node.js.
|
Open-source ticketing system — Request Tracker's paradigm rebuilt in modern TypeScript.
|
||||||
|
|
||||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
## Architecture
|
||||||
- Use `bun test` instead of `jest` or `vitest`
|
|
||||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
||||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
||||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
||||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
||||||
- Bun automatically loads .env, so don't use dotenv.
|
|
||||||
|
|
||||||
## APIs
|
```
|
||||||
|
tessera/
|
||||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
├── src/ # Backend: Bun + Hono + Drizzle ORM
|
||||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
│ ├── index.ts # Hono server entry (port 9876)
|
||||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
│ ├── config.ts # Zod-validated env config
|
||||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
│ ├── db/ # Drizzle ORM schema + migrations
|
||||||
- `WebSocket` is built-in. Don't use `ws`.
|
│ ├── routes/ # REST API endpoints
|
||||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
│ ├── models/ # TypeScript types + Zod schemas
|
||||||
- Bun.$`ls` instead of execa.
|
│ ├── scrip/ # Scrip engine (prepare/commit two-phase)
|
||||||
|
│ └── lifecycle/ # State machine validator
|
||||||
## Testing
|
├── web/ # Frontend: Next.js 15 + shadcn/ui
|
||||||
|
│ └── src/app/ # App Router pages
|
||||||
Use `bun test` to run tests.
|
├── drizzle/ # SQL migration files
|
||||||
|
└── docs/ # Architecture + design specs
|
||||||
```ts#index.test.ts
|
|
||||||
import { test, expect } from "bun:test";
|
|
||||||
|
|
||||||
test("hello world", () => {
|
|
||||||
expect(1).toBe(1);
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Frontend
|
## Stack
|
||||||
|
|
||||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
**Backend:** Bun runtime, Hono web framework, Drizzle ORM, PostgreSQL 17, Zod validation, Handlebars templates, nodemailer
|
||||||
|
|
||||||
Server:
|
**Frontend:** Next.js 15 App Router, shadcn/ui (Tailwind CSS), next-themes (light/dark), React Hook Form + Zod, TanStack Table, date-fns, lucide-react icons
|
||||||
|
|
||||||
```ts#index.ts
|
**Fonts:** Inter (variable, with cv01+ss03 OpenType features), JetBrains Mono
|
||||||
import index from "./index.html"
|
|
||||||
|
|
||||||
Bun.serve({
|
## Running Locally
|
||||||
routes: {
|
|
||||||
"/": index,
|
### Prerequisites
|
||||||
"/api/users/:id": {
|
- Bun (`nix-shell -p bun` or install globally)
|
||||||
GET: (req) => {
|
- Node.js 22+ (`nix-shell -p nodejs_22`)
|
||||||
return new Response(JSON.stringify({ id: req.params.id }));
|
- Docker (for PostgreSQL)
|
||||||
},
|
- PostgreSQL container: `docker run -d --name tessera-db -e POSTGRES_USER=tessera -e POSTGRES_PASSWORD=*** -e POSTGRES_DB=tessera -p 127.0.0.1:5433:5432 postgres:17-alpine`
|
||||||
},
|
|
||||||
},
|
### Start backend
|
||||||
// optional websocket support
|
```bash
|
||||||
websocket: {
|
cd ~/projects/tessera
|
||||||
open: (ws) => {
|
cp .env.example .env # Edit DATABASE_URL to point to postgres://tessera:***@127.0.0.1:5433/tessera
|
||||||
ws.send("Hello, world!");
|
bun run src/index.ts # Starts on port 9876
|
||||||
},
|
|
||||||
message: (ws, message) => {
|
|
||||||
ws.send(message);
|
|
||||||
},
|
|
||||||
close: (ws) => {
|
|
||||||
// handle close
|
|
||||||
}
|
|
||||||
},
|
|
||||||
development: {
|
|
||||||
hmr: true,
|
|
||||||
console: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
### Run migrations
|
||||||
|
```bash
|
||||||
```html#index.html
|
bun run src/db/migrate.ts
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<h1>Hello, world!</h1>
|
|
||||||
<script type="module" src="./frontend.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
With the following `frontend.tsx`:
|
### Start frontend
|
||||||
|
```bash
|
||||||
```tsx#frontend.tsx
|
cd web
|
||||||
import React from "react";
|
npm install # Use npm, NOT bun (bun has compatibility issues with Next.js dev server)
|
||||||
import { createRoot } from "react-dom/client";
|
npx next build # Production build
|
||||||
|
npx next start --port 5173 # Production server
|
||||||
// import .css files directly and it works
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const root = createRoot(document.body);
|
|
||||||
|
|
||||||
export default function Frontend() {
|
|
||||||
return <h1>Hello, world!</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
root.render(<Frontend />);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, run index.ts
|
**Note:** `bun run dev` (Turbopack) has WebSocket HMR issues in this environment. Use production mode only.
|
||||||
|
|
||||||
```sh
|
## API Endpoints
|
||||||
bun --hot ./index.ts
|
|
||||||
|
All endpoints are served by the backend on port 9876. The frontend proxies `/api/*` to the backend via `next.config.ts`.
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | /health | Health check |
|
||||||
|
| GET | /tickets | List tickets (?queue_id=&status=) |
|
||||||
|
| POST | /tickets | Create ticket |
|
||||||
|
| GET | /tickets/:id | Get ticket with custom fields |
|
||||||
|
| PATCH | /tickets/:id | Update ticket (validates lifecycle, runs scrips) |
|
||||||
|
| POST | /tickets/:id/preview | Dry-run scrips for status change |
|
||||||
|
| POST | /tickets/:id/comment | Add comment to ticket |
|
||||||
|
| GET | /tickets/:id/transactions | List ticket transactions |
|
||||||
|
| GET/POST | /queues | List/create queues |
|
||||||
|
| GET/POST/PATCH | /scrips | CRUD scrips |
|
||||||
|
| GET/POST | /custom-fields | List/create custom fields |
|
||||||
|
| GET/POST | /lifecycles | List/create lifecycles |
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
- **Ticket IDs are sequential integers** (1, 2, 3...), formatted as `TKT-0001` for display. No UUIDs.
|
||||||
|
- **Transaction-centric:** Every state change creates a transaction record. The scrip engine runs on transactions.
|
||||||
|
- **Two-phase scrip engine:** Prepare (no side effects) then Commit (execute actions). Supports dry-run mode.
|
||||||
|
- **Lifecycle state machines:** Per-queue configurable status transitions with wildcard support.
|
||||||
|
- **Light mode is default.** Dark mode available via theme toggle (next-themes).
|
||||||
|
- **No ORM for frontend:** Drizzle is only on the backend. Frontend uses a typed fetch wrapper (`web/src/lib/api.ts`).
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
Repo: `https://git.gjermund.xyz/gjermund/tessera`
|
||||||
|
|
||||||
|
Push via HTTPS with token auth (SSH port 2222 is not configured on Gitea):
|
||||||
|
```bash
|
||||||
|
git remote set-url origin https://gjermund:TOKEN@git.gjermund.xyz/gjermund/tessera.git
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
## Development Workflow
|
||||||
|
|
||||||
|
All code is written by **OpenCode** (AI coding agent). Hermes writes specs, OpenCode writes code, Gjermund reviews.
|
||||||
|
|
||||||
|
OpenCode server: `opencode serve --port 4096` (Gjermund attaches with `opencode --attach http://127.0.0.1:4096`)
|
||||||
|
|
||||||
|
After OpenCode makes changes:
|
||||||
|
1. `cd web && npx next build` — verify zero errors
|
||||||
|
2. `npx next start --port 5173` — restart production server
|
||||||
|
3. `git push` — push to origin
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
- **Frontend shows skeleton/blank page:** The frontend dev server (Turbopack) has WebSocket HMR issues. Use production mode (`npx next build` + `npx next start`).
|
||||||
|
- **Backend not running on 9876:** Restart with `bun run src/index.ts`. Check port with `ss -tlnp | grep 9876`.
|
||||||
|
- **Database connection refused:** Docker container may be stopped. `docker start tessera-db`.
|
||||||
|
- **Build errors after migration:** Run `bun run src/db/migrate.ts` to apply new migrations.
|
||||||
|
|||||||
Reference in New Issue
Block a user