claude code instructions

This commit is contained in:
2026-03-22 18:38:39 -04:00
parent 89bc8d00f7
commit 25a9fa4a02

362
CLAUDE.md Normal file
View File

@@ -0,0 +1,362 @@
# JAMA — Claude Code Development Context
## What is JAMA?
**jama** (just another messaging app) is a self-hosted, closed-source, full-stack Progressive Web App for team messaging. It supports both single-tenant (selfhost) and multi-tenant (host) deployments.
**Current version:** 0.11.25
---
## Stack
| Layer | Technology |
|---|---|
| Backend | Node.js + Express + Socket.io |
| Frontend | React + Vite (PWA) |
| Database | PostgreSQL 16 via `pg` npm package |
| Deployment | Docker Compose v2, Caddy reverse proxy (SSL) |
| Auth | JWT (cookie + localStorage), bcryptjs |
---
## Repository Layout
```
jama/
├── CLAUDE.md ← this file
├── KNOWN_LIMITATIONS.md
├── Dockerfile
├── build.sh
├── docker-compose.yaml
├── docker-compose.host.yaml
├── Caddyfile.example
├── .env.example
├── about.json.example
├── backend/
│ ├── package.json ← version bump required
│ └── src/
│ ├── index.js ← Express app, Socket.io, tenant middleware wiring
│ ├── middleware/
│ │ └── auth.js ← JWT auth, teamManagerMiddleware
│ ├── models/
│ │ ├── db.js ← Postgres pool, query helpers, migrations, seeding
│ │ └── migrations/ ← 001006 SQL files, auto-applied on startup
│ ├── routes/
│ │ ├── auth.js
│ │ ├── groups.js ← receives io
│ │ ├── messages.js ← receives io
│ │ ├── usergroups.js ← receives io
│ │ ├── schedule.js ← receives io (as of v0.11.14)
│ │ ├── users.js
│ │ ├── settings.js
│ │ ├── push.js
│ │ ├── host.js ← JAMA-HOST control plane only
│ │ ├── about.js
│ │ └── help.js
│ └── utils/
│ └── linkPreview.js
└── frontend/
├── package.json ← version bump required
├── vite.config.js
├── index.html
├── public/
│ ├── manifest.json
│ ├── sw.js ← service worker / push
│ └── icons/
└── src/
├── App.jsx
├── main.jsx
├── index.css
├── contexts/
│ ├── AuthContext.jsx
│ ├── SocketContext.jsx
│ └── ToastContext.jsx
├── pages/
│ ├── Chat.jsx ← main shell, page routing, all socket wiring
│ ├── Login.jsx
│ ├── ChangePassword.jsx
│ ├── UserManagerPage.jsx
│ ├── GroupManagerPage.jsx
│ └── HostAdmin.jsx ← DEAD CODE (safe to delete)
├── components/
│ ├── Sidebar.jsx ← conversation list, groupMessagesMode prop
│ ├── ChatWindow.jsx ← message thread + header
│ ├── MessageInput.jsx ← free-text compose, onTextChange prop
│ ├── Message.jsx ← single message renderer
│ ├── NavDrawer.jsx ← hamburger menu
│ ├── SchedulePage.jsx ← full schedule (~1600 lines, desktop+mobile views)
│ ├── MobileEventForm.jsx← mobile event create/edit
│ ├── Avatar.jsx ← avatar with consistent colour algorithm
│ ├── PasswordInput.jsx ← reusable show/hide password input
│ ├── GroupInfoModal.jsx
│ ├── ProfileModal.jsx
│ ├── SettingsModal.jsx
│ ├── BrandingModal.jsx
│ ├── HostPanel.jsx
│ ├── NewChatModal.jsx
│ ├── UserFooter.jsx
│ ├── GlobalBar.jsx
│ └── [others]
└── utils/
└── api.js ← all API calls + parseTS helper
```
---
## Version Bump — Files to Update
When bumping the version (e.g. 0.11.25 → 0.11.26), update **all three**:
```
backend/package.json "version": "X.Y.Z"
frontend/package.json "version": "X.Y.Z"
build.sh VERSION="${1:-X.Y.Z}"
```
One-liner:
```bash
OLD=0.11.25; NEW=0.11.26
sed -i "s/\"version\": \"$OLD\"/\"version\": \"$NEW\"/" backend/package.json frontend/package.json
sed -i "s/VERSION=\"\${1:-$OLD}\"/VERSION=\"\${1:-$NEW}\"/" build.sh
```
The `.env.example` has no version field. There is no fourth location.
---
## Output ZIP
When packaging for delivery: `jama.zip` at `/mnt/user-data/outputs/jama.zip`
Always exclude:
```bash
zip -qr jama.zip jama \
--exclude "jama/README.md" \
--exclude "jama/data/help.md" \
--exclude "jama/backend/src/data/help.md"
```
---
## Application Modes (APP_TYPE in .env)
| Mode | Description |
|---|---|
| `selfhost` | Single tenant — one schema `public`. Default if APP_TYPE unset. |
| `host` | Multi-tenant — one schema per tenant. Requires `HOST_DOMAIN` and `HOST_ADMIN_KEY`. |
JAMA-HOST tenants are provisioned at `{slug}.{HOST_DOMAIN}`. The host control panel lives at `https://{HOST_DOMAIN}/host`.
---
## Database Architecture
- **Pool:** `db.js``query(schema, sql, params)`, `queryOne`, `queryResult`, `exec`, `withTransaction`
- **Schema resolution:** `tenantMiddleware` sets `req.schema` from the HTTP `Host` header before any route runs. `assertSafeSchema()` validates all schema names against `[a-z_][a-z0-9_]*`.
- **Migrations:** Auto-run on startup via `runMigrations(schema)`. Files in `migrations/` applied in order, tracked in `schema_migrations` table per schema. **Never edit an applied migration — add a new numbered file.**
- **Seeding:** `seedSettings → seedEventTypes → seedAdmin → seedUserGroups` on startup. All use `ON CONFLICT DO NOTHING`.
- **Current migrations:** 001 (initial schema) → 002 (triggers/indexes) → 003 (tenants) → 004 (host plan) → 005 (U2U restrictions) → 006 (scrub deleted users)
---
## Socket Room Naming (tenant-isolated)
All socket rooms are prefixed with the tenant schema to prevent cross-tenant leakage:
```js
const R = (schema, type, id) => `${schema}:${type}:${id}`;
// e.g. R('tenant_acme', 'group', 42) → 'tenant_acme:group:42'
```
Room types: `group:{id}`, `user:{id}`, `schema:all` (tenant-wide broadcast).
Routes that emit socket events receive `io` as a function argument:
- `auth.js(io)`, `groups.js(io)`, `messages.js(io)`, `usergroups.js(io)`, `schedule.js(io)`
---
## Online User Tracking
```js
const onlineUsers = new Map(); // `${schema}:${userId}` → Set<socketId>
```
**Critical:** The map key is `${schema}:${userId}` — not bare `userId`. Integer IDs are per-schema, so two tenants can have the same user ID. Without the schema prefix, push notifications and online presence would leak across tenants.
---
## Active Sessions
Table: `active_sessions(user_id, device, token, ua)` — PK `(user_id, device)`
Device classes: `mobile` | `desktop` (from user-agent). One session per device type per user — logging in on the same device type displaces the previous session (socket receives `session:displaced`).
---
## Feature Flags & Plans
Stored in `settings` table per schema:
| Key | Values | Plan |
|---|---|---|
| `feature_branding` | `'true'`/`'false'` | Brand+ |
| `feature_group_manager` | `'true'`/`'false'` | Team |
| `feature_schedule_manager` | `'true'`/`'false'` | Team |
| `app_type` | `'JAMA-Chat'`/`'JAMA-Brand'`/`'JAMA-Team'` | — |
JAMA-HOST always forces `JAMA-Team` on the public schema at startup.
---
## Avatar Colour Algorithm
**Must be consistent across all three locations**`Avatar.jsx`, `Sidebar.jsx`, `ChatWindow.jsx`:
```js
const AVATAR_COLORS = ['#1a73e8','#ea4335','#34a853','#fa7b17','#a142f4','#00897b','#e91e8c','#0097a7'];
const bg = AVATAR_COLORS[(user.name || '').charCodeAt(0) % AVATAR_COLORS.length];
```
If you add a new surface that renders user avatars without a custom photo, use this exact algorithm.
---
## Key Frontend Patterns
### Page Navigation (Chat.jsx)
`page` state: `'chat'` | `'groupmessages'` | `'schedule'` | `'users'` | `'groups'` | `'hostpanel'`
**Rule:** Every page navigation must call `setActiveGroupId(null)` and `setChatHasText(false)` to clear the selected conversation and reset the unsaved-text guard.
### Group Messages vs Messages (Sidebar)
- `groupMessagesMode={false}` → shows public groups + non-managed private groups (PRIVATE MESSAGES section)
- `groupMessagesMode={true}` → shows only `is_managed` private groups (PRIVATE GROUP MESSAGES section)
- New chats always go to the Messages view; creating from Group Messages switches `setPage('chat')`
### Unsaved Text Guard (Chat.jsx → ChatWindow.jsx → MessageInput.jsx)
- `MessageInput` fires `onTextChange(val)` on every keystroke and after send
- `ChatWindow` converts to boolean via `onHasTextChange?.(!!val.trim())`
- `Chat.jsx` stores as `chatHasText`; `selectGroup()` shows `window.confirm` if true and switching conversations
- `MessageInput` resets all state (text, image, link preview) on `group?.id` change via `useEffect`
### Date/Time Utilities
Both `SchedulePage.jsx` and `MobileEventForm.jsx` maintain their own copies of:
- `roundUpToHalfHour()` — default start time for new events
- `parseTypedTime(raw)` — parses free-text time entry
- `fmt12(val)` — formats HH:MM as 12-hour display
- `toTimeIn(iso)` — extracts exact HH:MM from ISO (no rounding)
- `buildISO(date, time)` — builds timezone-aware ISO string for Postgres
`TimeInput` (desktop) and `TimeInputMobile` (mobile) are in-file components — free-text input with 5-slot scrollable dropdown showing only :00/:30 slots.
---
## User Deletion Behaviour
Deleting a user (v0.11.11+):
1. Email scrubbed to `deleted_{id}@deleted` — frees the address immediately
2. Name → `'Deleted User'`, display_name/avatar/about_me nulled, password cleared
3. All their messages set `is_deleted=TRUE, content=NULL, image_url=NULL`
4. Direct messages they were part of set `is_readonly=TRUE`
5. Group memberships, sessions, push subscriptions, notifications, event availability purged
Migration 006 back-fills this for pre-v0.11.11 deleted users.
Suspended users: sessions killed, login blocked, but all data intact and reversible.
---
## Notification Rules (Group Member Changes)
Handled in `usergroups.js` when Group Manager saves a user group's member list:
- **1 user added/removed** → named system message: `"{Name} has joined/been removed from the conversation."`
- **2+ users added/removed** → single generic message: `"N new members have joined/been removed from the conversation."`
Single-user add/remove via `groups.js` (GroupInfoModal) always uses the named message.
---
## Schedule / Event Rules
- **Date/time storage:** `TIMESTAMPTZ` in Postgres. All ISO strings from frontend must include timezone offset via `buildISO(date, time)`.
- **toTimeIn** preserves exact minutes (no half-hour snapping) for edit forms.
- **Default start time for new events:** `roundUpToHalfHour()` — current time rounded up to next :00 or :30.
- **Past start time rule:** New events (not edits) cannot have a start date/time in the past.
- **Recurring events:** `expandRecurringEvent` returns only occurrences within the requested range — never the raw original event as a fallback. Past occurrences are not shown.
- **Keyword filter:** Unquoted terms use `\bterm` (word-boundary prefix — `mount` matches `mountain`). Quoted terms use `\bterm\b` (exact whole-word — `"mount"` does not match `mountain`).
- **Type filter:** Does not shift the date window to today-onwards (unlike keyword/availability filters). Shows all matching events in the current month including past ones (greyed).
- **Clearing keyword:** Also resets `filterFromDate` so the view returns to the normal full-month display.
---
## Dead Code (safe to delete)
- `frontend/src/pages/HostAdmin.jsx` — replaced by `HostPanel.jsx`
- `frontend/src/components/UserManagerModal.jsx`
- `frontend/src/components/GroupManagerModal.jsx`
- `frontend/src/components/MobileGroupManager.jsx`
---
## Outstanding / Deferred Work
### Android Background Push (KNOWN_LIMITATIONS.md)
**Status:** Deferred. Web Push with VAPID doesn't survive Android Doze mode.
**Fix plan:** Integrate Firebase Cloud Messaging (FCM).
1. Create Firebase project (free tier)
2. Add Firebase config to `.env` and `sw.js`
3. Replace `web-push` subscription flow with Firebase SDK
4. Switch backend dispatch from `web-push` to `firebase-admin`
5. WebSocket reconnect-on-focus (frontend only, no Firebase needed)
### WebSocket Reconnect on Focus
**Status:** Deferred. Socket drops when Android PWA is backgrounded.
**Fix:** Frontend-only — listen for `visibilitychange` in `SocketContext.jsx`, reconnect socket when `document.visibilityState === 'visible'`.
---
## Environment Variables (.env.example)
Key variables:
```
APP_TYPE=selfhost|host
HOST_DOMAIN= # host mode only
HOST_ADMIN_KEY= # host mode only
JWT_SECRET=
DB_HOST=db
DB_NAME=jama
DB_USER=jama
DB_PASSWORD= # avoid ! (shell interpolation issue with docker-compose)
ADMIN_EMAIL=
ADMIN_NAME=
ADMIN_PASS=
ADMPW_RESET=true|false
APP_NAME=jama
USER_PASS= # default password for bulk-created users
DEFCHAT_NAME=General Chat
JAMA_VERSION= # injected by build.sh into Docker image
VAPID_PUBLIC= # auto-generated on first start if not set
VAPID_PRIVATE= # auto-generated on first start if not set
```
---
## Deployment
```bash
# Production: Ubuntu 22.04, Docker Compose v2
# Directory: /home/rick/jama/
./build.sh # builds Docker image
docker compose up -d # starts all services
```
Build sequence: `build.sh` → Docker build → `npm run build` (Vite) → `docker compose up -d`
---
## Session History
Previous development was conducted via Claude.ai web interface (sessions summarised in this document). Development continues in Claude Code from v0.11.25.