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

4.3 KiB

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

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.