Files
numolus/CLAUDE.md
T
wkhandClaude Opus 5.5 0368e96e0c Merge the server concept into the README; add CLAUDE.md
The README gets a chapter on storage, accounts and live collaboration
that describes what is built (local vs server documents, flows, roles,
architecture, Yjs layer, channel protocol, data model, API, .env
configuration, security, open points). The planning document with its
outdated parts (guest accounts, y-rb, edit lock, phases) is removed.
CLAUDE.md collects commands, layout and the rules that are easy to break.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:00:44 +02:00

85 lines
4.3 KiB
Markdown

# CLAUDE.md
numolus: spreadsheet à la Apple Numbers (free canvas with table/chart/text/button
objects) with a hand-written formula engine. Branch `with-server` = Rails 8 app
with the Vue 3 frontend in `app/frontend/`; accounts, sharing, live collaboration.
Details for users and architecture: README.md, chapter "Speichern, Konten &
Zusammenarbeit".
## Branches
- `main` (tag `client-only`): browser-only app, documents in IndexedDB. Keep it
free of server code; fixes that apply to both go in their own commit so they
can be cherry-picked.
- `with-server`: everything below.
## Commands
```bash
bin/setup # gems, npm packages, database
bin/dev # Rails :3000 + Vite dev server :3036 (open localhost:3000)
npm test # Vitest (engine, store, io, collab)
bin/rails test # Minitest (models, API, auth, sharing, DocumentChannel)
npx vue-tsc --noEmit -p . # typecheck
npm run lint # ESLint
npm run build # typecheck + production build into public/vite
bin/brakeman -q --no-pager
```
Run all of them before committing. Mails in development: localhost:3000/letter_opener.
## Layout
- `app/frontend/engine/` formula engine (framework-agnostic, own README)
- `app/frontend/store/document.ts` the document store (~3300 lines, one open document)
- `app/frontend/store/library.ts` which document is open, local (IndexedDB) vs server, URLs
- `app/frontend/store/session.ts` signed-in user; `io/api.ts` REST client; `io/library.ts` IndexedDB
- `app/frontend/collab/` live collaboration: `ydoc.ts` (store ↔ Yjs), `session.ts` (ActionCable, undo, awareness)
- `app/channels/document_channel.rb` stores/relays Yjs updates, checks access
- `app/models/document.rb` `role_for(user:, token:)` is the one access check
## Rules that are easy to break
- **Cell addressing stays positional**: `TableObject.cells["col,row"]` in store,
engine and file format (`.num`/`.json`). Stable row/column ids exist only in
the Yjs layer (`collab/ydoc.ts`); don't change the file format for them.
- **Every content-changing store function calls `markDirty()`** (selection-only
changes call `scheduleSave()`), and must be listed in `READ_ONLY_GUARDED` in
`document.ts`, else viewers of a shared document can change it locally.
- **Mid-table row/column insert/delete must call `collabHooks?.onStructure(...)`**
(as `insertRow`/`deleteRow`/`insertColumn`/`deleteColumn` do). Edge growth/shrink
needs nothing. Without it, concurrent edits land in the wrong row.
- View state is per person: `selection`, `activeSheetId`, `activeObjectId`, sheet
`zoom` are not synced. `applyExternalState` keeps them when others' changes arrive.
- Remote changes: flush pending local changes into Yjs first, then apply, then
rebuild the store (see `CollabSession.process`). Keep that order.
- Local documents persist via `setPersistHandler` (IndexedDB); server documents
only via the collab session. Undo: snapshots locally, `Y.UndoManager` for server docs.
- Solid Queue/Cache/Cable live in the same PostgreSQL database as the app data
(plain migrations in `db/migrate`), no separate databases (user decision).
- Settings and secrets come from `.env` (gem `dotenv`, template `.env.example`),
never committed.
- Share link tokens are stored in plain text on purpose (owner can copy the link
again); magic link tokens/codes only as SHA-256.
## Conventions
- UI text and user-facing messages in German; code, comments, commit messages in
English. Comments explain *why*, in the surrounding (fairly thorough) style.
- Commit messages: short imperative subject, body with the reasons.
- The user sometimes commits on their own: check `git status`/`git log` fresh
before committing; don't push unless asked.
## Environment gotchas
- `vite-plugin-ruby` makes `app/frontend` the Vite root: Vitest `include`
patterns in `vite.config.ts` are relative to it (`**/*.spec.ts`).
- Vite is pinned to v5 (Vitest 2 and `@vitejs/plugin-vue` 5 don't support newer);
`vite install`/`vite upgrade` may bump it again.
- Gems install into `vendor/gems` (global `BUNDLE_PATH`), ignored by git.
- Node comes from mise; outside the project directory scripts may need
`MISE_NODE_VERSION=22.21.1`.
- Browser checks: there is no Playwright in the repo; drive Chrome
(`/Applications/Google Chrome.app`) with `playwright-core` from a scratch
directory against `bin/dev`.