Add Rails backend: accounts via magic link, server documents, sharing

The app becomes a Rails 8 application; the Vue frontend moves to
app/frontend and is served through vite_rails.

- Without an account documents stay in the browser (IndexedDB). Sign-in
  is by magic link or the 6-digit code from the same mail; afterwards
  local documents can be uploaded into the account.
- Server documents (/d/<id>) with versioned saves and a single-editor
  lock per browser tab; others see them read-only and follow by polling.
- Sharing via link (works without an account) and via e-mail invitation,
  roles viewer/editor; the owner manages links and people.
- Store gains a read-only mode that blocks all content changes.
- Solid Queue, Cache and Cable share the one PostgreSQL database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-28 20:09:06 +02:00
co-authored by Claude Opus 5.5
parent 5a0f1fcd78
commit e14bada114
190 changed files with 6077 additions and 722 deletions
+27 -14
View File
@@ -4,21 +4,34 @@ Eine browserbasierte Tabellenkalkulation nach dem Vorbild von **Apple Numbers**.
1. **Handwerk** — eine von Grund auf selbst geschriebene, schlanke Formel-Engine
(Tokenizer, Parser, AST, Abhängigkeitsgraph, Neuberechnung, Zirkelbezug-
Erkennung). Dokumentiert in [`src/engine/README.md`](src/engine/README.md).
Erkennung). Dokumentiert in [`app/frontend/engine/README.md`](app/frontend/engine/README.md).
2. **Produkt** — eine polierte, Numbers-nahe Oberfläche: jedes Blatt ist eine
**freie Fläche (Canvas)**, auf die man beliebig viele **Tabellen- und
Diagramm-Objekte** setzt, verschiebt, skaliert und befüllt.
```bash
npm install
npm run dev # Vite-Devserver
npm run test # Vitest-Suite (Engine + Store + IO), 570 Tests
npm run build # Typecheck + Produktionsbuild
bin/setup # Gems, npm-Pakete, Datenbank (PostgreSQL) anlegen
bin/dev # Rails (Port 3000) + Vite-Devserver mit HMR (Port 3036)
npm run test # Vitest-Suite (Engine + Store + IO)
bin/rails test # Server-Tests (Models, API, Anmeldung, Teilen)
npm run build # Typecheck + Produktionsbuild nach public/vite
npm run lint # ESLint (flat config)
```
Alles läuft **rein clientseitig** — kein Server, keine Netzwerk-Requests. Datei-
I/O über File API und Blob-URLs.
Die App ist eine **Rails-8-Anwendung** mit dem Vue-Frontend unter
[`app/frontend/`](app/frontend/) (eingebunden über `vite_rails`). Ohne Konto
bleibt alles **im Browser** (IndexedDB). Mit Konto (Anmeldung per Magic Link,
kein Passwort) liegen Dokumente auf dem Server und lassen sich teilen – per
Link oder per Einladung, mit den Rollen „ansehen“ und „bearbeiten“. Solange
es keine Live-Zusammenarbeit gibt, bearbeitet immer nur ein Browser-Tab ein
Server-Dokument; alle anderen sehen es schreibgeschützt und folgen den
Änderungen. Details: [`docs/konzept-teilen-und-zusammenarbeit.md`](docs/konzept-teilen-und-zusammenarbeit.md).
Mails landen in der Entwicklung nicht im Postfach, sondern unter
<http://localhost:3000/letter_opener>. Produktion braucht `SMTP_ADDRESS`,
`SMTP_USER_NAME`, `SMTP_PASSWORD`, `NUMOLUS_MAIL_FROM`, `NUMOLUS_HOST` und
`NUMOLUS_DATABASE_PASSWORD`; Solid Queue, Cache und Cable liegen in derselben
Datenbank wie die Daten.
---
@@ -268,7 +281,7 @@ eingebauter Funktionsname: `=MARGE(Umsatz!B2; Umsatz!C2)`.
sich automatisch umschreiben ließe, nur ihren Aufruf-Namen.
- Ein Aufruf wird **beim Parsen der aufrufenden Zelle in ihre AST eingesetzt**
(Inlining, keine Laufzeit-Closures) — siehe
[`src/engine/userFunctions.ts`](src/engine/userFunctions.ts) und die
[`app/frontend/engine/userFunctions.ts`](app/frontend/engine/userFunctions.ts) und die
Architektur-Tabelle unten. Dadurch braucht Evaluator/Graph keine Änderung,
und auch Zellen, die *nur* im Funktionskörper referenziert werden (nicht im
Aufruf), lösen bei Änderung korrekt eine Neuberechnung aus.
@@ -336,7 +349,7 @@ berechenbar ist. Eine `FOR`-Schleife über insgesamt mehr als 10.000 Zellen
Funktion, deren Ausführung endet, ohne dass `RETURN` je gefeuert hat,
ebenso — das Formular warnt davor zusätzlich schon **beim Speichern**, wenn
nicht erkennbar jeder Codepfad ein `RETURN` erreicht. Details:
[`src/engine/README.md`](src/engine/README.md)s „Schleifen"-Abschnitt.
[`app/frontend/engine/README.md`](app/frontend/engine/README.md)s „Schleifen"-Abschnitt.
#### SUMIF/COUNTIF/AVERAGEIF nachbauen
@@ -644,7 +657,7 @@ unabhängig von der Locale), **Zell-Textfarbe**,
### Einstellungen (⚙, links neben dem Titel)
Ein Zahnrad neben der Überschrift öffnet eine linke Inspector-Schublade
([`SettingsPanel.vue`](src/components/SettingsPanel.vue)), die die Fläche zur
([`SettingsPanel.vue`](app/frontend/components/SettingsPanel.vue)), die die Fläche zur
Seite schiebt (auf schmalen Viewports überlagert sie). Sie bündelt alle
wichtigen Punkte:
@@ -1034,7 +1047,7 @@ Fehlercode nötig).
## Architektur der Engine-Module
Framework-agnostisches ES-Modul unter [`src/engine/`](src/engine/) — **keine**
Framework-agnostisches ES-Modul unter [`app/frontend/engine/`](app/frontend/engine/) — **keine**
Formel-/Spreadsheet-Library. Kennt kein Vue, kein DOM. Die adressierbare Einheit
ist eine **Tabelle**; der Abhängigkeitsgraph läuft dokumentweit über alle
Tabellen aller Blätter.
@@ -1054,7 +1067,7 @@ Tabellen aller Blätter.
`graph.topoOrder()` → Zyklus-Zellen `#CIRC`, Rest topologisch auswerten, nicht
betroffene Zellen behalten ihren Wert.
**Anbindung an Vue.** [`src/store/document.ts`](src/store/document.ts) ist das
**Anbindung an Vue.** [`app/frontend/store/document.ts`](app/frontend/store/document.ts) ist das
„eigene `reactive()`-Modul": es hält eine `Workbook`-Instanz und einen
`recalcTick`-Ref. Die UI liest `store.cellValue(...)`, das `recalcTick` berührt —
nach jeder Neuberechnung wird der Tick erhöht, jede Formelzelle re-rendert also
@@ -1123,7 +1136,7 @@ Blatt **„Analyse"** mit drei Tabellen-Objekten (**Umsatz**, **Kosten**,
**Übersicht**) und einem **Diagramm** (Umsatz je Region), plus ein leeres Blatt
**„Blatt 2"**. „Übersicht" demonstriert tabellenübergreifende Bezüge und die vier
Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
`E2 = =D2/B2*100`. Siehe [`src/data/seed.ts`](src/data/seed.ts).
`E2 = =D2/B2*100`. Siehe [`app/frontend/data/seed.ts`](app/frontend/data/seed.ts).
> **Hinweis zur Aufgabenstellung:** Die Akzeptanzliste nennt „Übersicht!B2 =
> 6070". Die explizit angegebene Nord-Zeile ergibt jedoch
@@ -1498,7 +1511,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
## Projektstruktur
```
src/
app/frontend/
engine/ # Formel-Engine (Eigenbau) + Unit-Tests
store/ # reaktiver Dokument-Store (v2 Canvas-Modell), Formatierung
io/ # csv / json (Eigenbau, inkl. v1→v2), num (ZIP via fflate, lazy), xlsx (SheetJS, lazy)