From 25a9fa4a02b0dda128e9665a84ac5dc6400a336e Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sun, 22 Mar 2026 18:38:39 -0400 Subject: [PATCH] claude code instructions --- CLAUDE.md | 362 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6a5bf80 --- /dev/null +++ b/CLAUDE.md @@ -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 +``` + +**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.