claude code instructions
This commit is contained in:
362
CLAUDE.md
Normal file
362
CLAUDE.md
Normal 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/ ← 001–006 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.
|
||||||
Reference in New Issue
Block a user