# 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`.