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>
This commit is contained in:
@@ -26,14 +26,12 @@ Link oder per Einladung, mit den Rollen „ansehen“ und „bearbeiten“. Alle
|
||||
Bearbeiten-Recht arbeiten **gleichzeitig** am selben Dokument: Änderungen
|
||||
kommen über ActionCable sofort bei den anderen an (Yjs, siehe
|
||||
[`app/frontend/collab/`](app/frontend/collab/)), jede Person hat ihr eigenes
|
||||
Undo, und die Auswahl der anderen ist als farbiger Rahmen zu sehen. Details: [`docs/konzept-teilen-und-zusammenarbeit.md`](docs/konzept-teilen-und-zusammenarbeit.md).
|
||||
Undo, und die Auswahl der anderen ist als farbiger Rahmen zu sehen. Details:
|
||||
[Speichern, Konten & Zusammenarbeit](#speichern-konten--zusammenarbeit).
|
||||
|
||||
**Konfiguration:** Datenbank-Zugang, Server-Name, SMTP und `SECRET_KEY_BASE`
|
||||
stehen in einer `.env` im Projektverzeichnis – Vorlage kopieren und ausfüllen:
|
||||
`cp .env.example .env`. Die Datei wird nie committet; echte
|
||||
Umgebungsvariablen haben Vorrang. Solid Queue, Cache und Cable liegen in
|
||||
derselben Datenbank wie die Daten. Mails landen in der Entwicklung nicht im
|
||||
Postfach, sondern unter <http://localhost:3000/letter_opener>.
|
||||
stehen in einer `.env` im Projektverzeichnis: `cp .env.example .env` und
|
||||
ausfüllen (siehe [Konfiguration & Betrieb](#konfiguration--betrieb)).
|
||||
|
||||
---
|
||||
|
||||
@@ -179,7 +177,7 @@ Texteingabe (Zell-Editor, Formelleiste, Blatt-Umbenennen, …) den nativen
|
||||
Undo dieses Feldes benutzen soll.
|
||||
|
||||
**Performance bei großen Tabellen**: Undo-Tracking und die (debounced)
|
||||
`localStorage`-Persistenz werden **nicht** über einen `watch(doc, ...,
|
||||
Speicherung werden **nicht** über einen `watch(doc, ...,
|
||||
{deep:true})` angestoßen — Vues Deep-Watch muss dafür bei *jeder* Mutation den
|
||||
kompletten reaktiven Objektgraphen erneut durchlaufen, ein Aufwand
|
||||
proportional zur Gesamtgröße des Dokuments statt zur eigentlichen Änderung
|
||||
@@ -700,8 +698,8 @@ Zeilen-/Spaltengrenzen selbst ein und wird nicht zusätzlich pixelweise
|
||||
gerastert.
|
||||
|
||||
Alle diese Einstellungen sind Teil des Dokuments (`DocumentState` /
|
||||
`Sheet`) und werden damit in **localStorage gespeichert und in `.json`
|
||||
exportiert** – Round-Trip verlustfrei, ungültige Werte werden beim Import
|
||||
`Sheet`) und werden damit **mit dem Dokument gespeichert und in `.json`
|
||||
exportiert** (der Zoom bleibt bei geteilten Dokumenten pro Person) – Round-Trip verlustfrei, ungültige Werte werden beim Import
|
||||
verworfen bzw. geklemmt.
|
||||
|
||||
### Zahlenformat (Ländereinstellung)
|
||||
@@ -1112,7 +1110,8 @@ Alles clientseitig (`Blob` + `URL.createObjectURL` + `<a download>` + revoke).
|
||||
### Import
|
||||
`<input type="file">` **und** Drag & Drop auf die Canvas; `.num` `.json` `.csv` `.xlsx`.
|
||||
|
||||
- **`.num`** / **`.json`** → Dokument *ersetzen* (Rückfrage) oder Blätter *anhängen*.
|
||||
- **`.num`** / **`.json`** → *als neues Dokument* öffnen (Standard), das offene
|
||||
Dokument *ersetzen* (Rückfrage) oder Blätter *anhängen*.
|
||||
**v1-Dokumente** (altes Ein-Raster-Format) werden automatisch nach v2
|
||||
migriert (je altes Blatt ein Blatt mit einer gleichnamigen Tabelle). Schema
|
||||
wird validiert; bei Fehler klare Meldung, **kein Teil-Import**.
|
||||
@@ -1123,14 +1122,263 @@ Alles clientseitig (`Blob` + `URL.createObjectURL` + `<a download>` + revoke).
|
||||
- Nach jedem Import: kompletter Reparse + Neuberechnung + Diagramm-Refresh.
|
||||
Schlägt ein Import fehl, bleibt das bestehende Dokument **unverändert**.
|
||||
|
||||
## Persistenz
|
||||
## Speichern, Konten & Zusammenarbeit
|
||||
|
||||
Das gesamte Dokument wird (debounced) nach `localStorage` gespiegelt, robust
|
||||
gegen leeren/kaputten State — Fallback ist ein **leeres Dokument** (ein Blatt
|
||||
„Blatt 1" mit einer leeren „Tabelle 1"). Ein v1-State im Storage wird beim
|
||||
Laden migriert. **Datei › Neu** legt ein neues leeres Dokument an, **Datei ›
|
||||
Muster · Umsatzanalyse** lädt das vorbefüllte Musterdokument (beides nach
|
||||
Rückfrage, die bisherigen Änderungen gehen verloren).
|
||||
Ohne Konto bleibt alles **im Browser**. Ein Konto braucht es nur, wenn man
|
||||
seine Dokumente auf mehreren Geräten haben oder sie mit anderen teilen will.
|
||||
Mit Konto liegen Dokumente auf dem Server und werden **live gemeinsam**
|
||||
bearbeitet.
|
||||
|
||||
### Grundsätze
|
||||
|
||||
- **Anonym zuerst.** App öffnen, arbeiten, ohne Registrierung. Kein
|
||||
Gast-Konto auf dem Server.
|
||||
- **Konto nur bei Bedarf**, Anmeldung nur per **Magic Link** (E-Mail, dazu ein
|
||||
6-stelliger Code). Keine Passwörter. Die erste Anmeldung legt das Konto an.
|
||||
- **Nichts geht verloren.** Nach der ersten Anmeldung fragt die App, welche
|
||||
lokalen Dokumente ins Konto hochgeladen werden sollen.
|
||||
- **Links sind Zugänge.** Ein Lesen- oder Bearbeiten-Link funktioniert auch
|
||||
ohne Konto und lässt sich jederzeit widerrufen.
|
||||
- **Gleichzeitig statt nacheinander.** Änderungen werden per CRDT (Yjs)
|
||||
zusammengeführt, ohne Sperren; jede Person hat ihr eigenes Undo.
|
||||
- **Dateien bleiben.** `.num`, `.json`, CSV und XLSX bleiben als
|
||||
Import/Export.
|
||||
|
||||
### Wo Dokumente liegen
|
||||
|
||||
| | Lokal | Server |
|
||||
| --- | --- | --- |
|
||||
| Wer | alle, auch ohne Konto | mit Konto, oder per Link |
|
||||
| Wo | IndexedDB dieses Browsers | PostgreSQL |
|
||||
| Adresse | `/?doc=<id>` | `/d/<uuid>` (Link: `/d/<uuid>#k=<token>`) |
|
||||
| Speichern | automatisch, 250 ms nach der Änderung | live, jede Änderung sofort an alle |
|
||||
| Undo | Snapshots des Dokuments | pro Person (`Y.UndoManager`) |
|
||||
| Teilen | – (erst hochladen) | per Link oder Einladung |
|
||||
|
||||
**Datei › Dokumente …** listet alles in bis zu drei Gruppen: *Mein Konto*,
|
||||
*Mit mir geteilt* (Einladungen und Links) und *In diesem Browser*. Dort lässt
|
||||
sich öffnen, umbenennen, duplizieren, löschen bzw. aus der Liste entfernen und
|
||||
ein lokales Dokument ins Konto hochladen. Der Dokumentname steht in der
|
||||
Toolbar und ist direkt umbenennbar; er ist auch der Dateiname beim Export.
|
||||
**Neues Dokument** und **Muster** legen je ein weiteres Dokument an: mit Konto
|
||||
auf dem Server, sonst lokal. Ein alter Einzel-Speicherstand aus
|
||||
`localStorage` wird beim ersten Start als „Mein Dokument“ übernommen.
|
||||
|
||||
**Mehrere Tabs:** Jeder Tab kann ein anderes Dokument offen haben. Dieselbe
|
||||
lokale Datei in zwei Tabs folgt den Speicherungen des anderen Tabs
|
||||
(`BroadcastChannel`); tippen beide gleichzeitig, gewinnt die letzte
|
||||
Speicherung, darauf weist ein Hinweis hin. Server-Dokumente führen
|
||||
gleichzeitige Änderungen immer zusammen.
|
||||
|
||||
### Abläufe
|
||||
|
||||
1. **Anonym arbeiten** – Dokumente liegen in IndexedDB. Ein anderer Browser
|
||||
oder gelöschte Browserdaten bedeuten: diese Dokumente sind dort nicht da.
|
||||
2. **Anmelden** – E-Mail eingeben, Mail mit Link und Code. Der Link öffnet eine
|
||||
Bestätigungsseite; angemeldet wird erst per Klick (`POST`), damit
|
||||
Mail-Scanner, die Links vorab aufrufen, ihn nicht verbrauchen. Alternativ
|
||||
den Code in der App eingeben (Link am Handy, App am Desktop). Danach: Rückfrage
|
||||
zum Hochladen lokaler Dokumente; hochgeladene werden lokal entfernt.
|
||||
3. **Teilen per Link** – *Teilen* → Link „kann ansehen“ bzw. „kann bearbeiten“
|
||||
erstellen und kopieren. Funktioniert ohne Konto; der Token wird im Browser
|
||||
gemerkt (`localStorage`) und nach einer späteren Anmeldung dem Konto
|
||||
zugeordnet. *Neu* erzeugt einen neuen Link; der alte und alle darüber
|
||||
entstandenen Zugänge sind dann ungültig.
|
||||
4. **Einladen** – E-Mail + Rolle. Die Einladung (7 Tage gültig) meldet beim
|
||||
Öffnen gleichzeitig an und legt bei Bedarf das Konto an.
|
||||
5. **Gemeinsam bearbeiten** – Änderungen kommen in ~150 ms bei den anderen an.
|
||||
Die Auswahl der anderen erscheint als farbiger Rahmen mit Namen, oben
|
||||
rechts die Avatare. Anonyme Personen heißen zwei zufällige Begriffe
|
||||
(„Quelle Mond“). Undo nimmt nur eigene Änderungen zurück. Wer nur lesen
|
||||
darf, sieht alles live, kann aber nichts ändern („Nur Lesen“).
|
||||
|
||||
Ein lokales Dokument teilen: *Teilen* → ohne Konto erst anmelden, dann
|
||||
*Hochladen und teilen*.
|
||||
|
||||
### Rollen
|
||||
|
||||
| Aktion | Besitzer | Bearbeiten | Ansehen |
|
||||
| --- | --- | --- | --- |
|
||||
| Lesen, Anwesenheit der anderen sehen | ja | ja | ja |
|
||||
| Bearbeiten, Umbenennen | ja | ja | nein |
|
||||
| Exportieren, Duplizieren (ins eigene Konto) | ja | ja | ja |
|
||||
| Links und Personen verwalten | ja | nein | nein |
|
||||
| Löschen | ja | – | – |
|
||||
| Aus der eigenen Liste entfernen | – | ja | ja |
|
||||
|
||||
Zugang entsteht durch **Besitz** (wer anlegt), **Einladung** (Mitgliedschaft
|
||||
`invite`, bleibt beim Widerruf eines Links bestehen) oder **Link**
|
||||
(angemeldet: Mitgliedschaft `link`; anonym: nur der Token im Browser). Ein
|
||||
Link schwächt nie eine bestehende Rolle ab. Der Token steht im URL-Fragment
|
||||
(`#k=…`), das Browser nicht an Server schicken; die App entfernt es nach dem
|
||||
Einlösen aus der Adresszeile.
|
||||
|
||||
Geprüft wird zentral über `Document#role_for(user:, token:)`: im REST-API per
|
||||
Controller (kein Zugang → `404`, nicht `403`), im WebSocket beim Verbinden.
|
||||
Ändern sich Links oder Mitgliedschaften, prüft jede offene Verbindung ihre
|
||||
Rolle sofort neu: Wer den Zugang verliert, wird getrennt; wer herabgestuft
|
||||
wird, ist sofort im Nur-Lesen-Modus.
|
||||
|
||||
### Architektur
|
||||
|
||||
```
|
||||
┌─ Browser (je Person) ─────────┐ ┌─ Rails 8 ──────────────────────┐
|
||||
│ Vue-App (app/frontend) │ HTTPS │ REST-API /api, Login /auth │ ┌────────────┐
|
||||
│ Store (DocumentState, │◄────────►│ Magic Link, Liste, Teilen │────►│ PostgreSQL │
|
||||
│ positionsbasiert "c,r") │ │ │ │ Daten + │
|
||||
│ ▲ ▼ collab/ydoc.ts │ WebSocket│ DocumentChannel (ActionCable) │────►│ Solid Queue│
|
||||
│ Y.Doc (feste Zeilen-/ │◄────────►│ speichert + verteilt Updates │ │ Cache,Cable│
|
||||
│ Spalten-IDs), Awareness │ /cable │ │ └────────────┘
|
||||
│ IndexedDB (lokale Dokumente) │ │ Mailer (Solid Queue) ──► SMTP │
|
||||
└───────────────────────────────┘ └────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Rails 8** mit dem Vue-Frontend über `vite_rails`; `SpaController` liefert
|
||||
die App für `/` und `/d/:id` und bettet den angemeldeten User ein
|
||||
(`#numolus-boot`).
|
||||
- **Solid Queue, Solid Cache, Solid Cable** liegen als normale Migrationen in
|
||||
**derselben** PostgreSQL-Datenbank wie die Daten; kein Redis.
|
||||
- Der Server versteht den Inhalt der Yjs-Updates nicht und muss es nicht: er
|
||||
speichert und verteilt sie nur. Formeln rechnet weiterhin jeder Browser selbst.
|
||||
|
||||
### Live-Bearbeitung (Yjs)
|
||||
|
||||
Der **Store bleibt unverändert positionsbasiert** (`cells["col,row"]`), Engine
|
||||
und Dateiformat ebenso. Die Schicht [`app/frontend/collab/`](app/frontend/collab/)
|
||||
übersetzt:
|
||||
|
||||
- [`ydoc.ts`](app/frontend/collab/ydoc.ts) – Yjs-Form des Dokuments und die
|
||||
Übersetzung in beide Richtungen. Jede Tabelle hat `rowOrder`/`colOrder` mit
|
||||
**festen IDs**; Zellen, Formate, Breiten und Höhen hängen an diesen IDs
|
||||
(`"colId|rowId"`). `Binding.sync()` schreibt nur, was sich geändert hat
|
||||
(eine Zelle, eine Eigenschaft), damit Änderungen verschiedener Personen
|
||||
zusammengeführt statt überschrieben werden. Zeile/Spalte einfügen oder
|
||||
löschen melden die vier Store-Funktionen `insertRow`/`deleteRow`/
|
||||
`insertColumn`/`deleteColumn` ausdrücklich (`onStructure`), denn aus
|
||||
verschobenen Zellen allein ist das nicht erkennbar. So landet eine Eingabe
|
||||
in Zeile 7 in der richtigen Zeile, auch wenn gleichzeitig jemand darüber
|
||||
eine Zeile einfügt.
|
||||
- [`session.ts`](app/frontend/collab/session.ts) – eine Sitzung je offenem
|
||||
Server-Dokument: ActionCable-Abo, `Y.UndoManager` (nur eigene Änderungen),
|
||||
Awareness (Name, Farbe, Auswahl als Zeilen-/Spalten-IDs). Reihenfolge ist
|
||||
wichtig: Vor jeder fremden Änderung gehen eigene, noch nicht übertragene
|
||||
Änderungen zuerst nach Yjs; danach wird der Store mit
|
||||
`applyExternalState` neu aufgebaut, wobei Auswahl, aktives Blatt und Zoom
|
||||
der Person erhalten bleiben (die eigene Auswahl folgt eingefügten Zeilen).
|
||||
- **Erste Umwandlung** eines JSON-Dokuments in Yjs geschieht im Browser,
|
||||
deterministisch (feste Client-ID, IDs `r0…`/`c0…`): Öffnen zwei Personen es
|
||||
gleichzeitig, entstehen identische Updates, die Yjs zu einem verschmilzt.
|
||||
- **Zusammenfassen:** Ab 200 gespeicherten Updates schickt ein Editor den
|
||||
Gesamtstand; der Server ersetzt genau die aufgelisteten Updates durch ihn.
|
||||
Editoren halten außerdem `documents.content` als JSON-Snapshot aktuell
|
||||
(alle 30 s bei Änderung, beim Verlassen, beim Zusammenfassen) – für
|
||||
Duplizieren, Export und die Liste.
|
||||
- **Wiederverbinden:** ActionCable verbindet neu, der Server schickt alle
|
||||
Updates, der Browser schickt, was dem Server fehlt. Anzeige „Offline“ in der
|
||||
Toolbar, solange die Verbindung weg ist.
|
||||
- **Zoom** ist pro Person und wird nicht geteilt; Auswahl, aktives Blatt und
|
||||
aktives Objekt ebenso nicht. Die Z-Reihenfolge (Klick holt ein Objekt nach
|
||||
vorn) ist Dokumentinhalt; im Nur-Lesen-Modus ändert ein Klick sie nicht.
|
||||
|
||||
**Nur-Lesen im Store:** Ist `store.ui.readOnly` gesetzt, sind alle
|
||||
inhaltsändernden Store-Funktionen wirkungslos (Liste `READ_ONLY_GUARDED` in
|
||||
[`document.ts`](app/frontend/store/document.ts)); ein Versuch zeigt den Grund als Hinweis.
|
||||
|
||||
**DocumentChannel** ([`document_channel.rb`](app/channels/document_channel.rb)),
|
||||
Binärdaten Base64:
|
||||
|
||||
| Richtung | Nachricht | Inhalt |
|
||||
| --- | --- | --- |
|
||||
| Browser → Server | `update` | ein Yjs-Update (nur Editoren) |
|
||||
| Browser → Server | `awareness` | Auswahl/Name, wird nicht gespeichert |
|
||||
| Browser → Server | `compact` | Gesamtstand + Liste der ersetzten Update-IDs + JSON-Snapshot |
|
||||
| Browser → Server | `snapshot` | JSON-Snapshot |
|
||||
| Server → Browser | `init` | Rolle, Titel, alle Updates; solange es keine gibt: das JSON zum Umwandeln |
|
||||
| Server → Browser | `update`, `awareness`, `compacted` | weitergereicht bzw. bestätigt |
|
||||
| Server → Browser | `meta`, `role`, `revoked` | Titel geändert, Rolle geändert, Zugang weg |
|
||||
|
||||
### Datenmodell
|
||||
|
||||
| Tabelle | Wichtige Spalten |
|
||||
| --- | --- |
|
||||
| `users` | `email` (citext, eindeutig), `display_name`, `color`, `last_seen_at` |
|
||||
| `sessions` | `user_id`, `user_agent`, `ip_address`, `last_active_at` – signierte Session-ID im Cookie |
|
||||
| `magic_links` | `email`, `token_digest`, `code_digest` (nur SHA-256), `purpose` (login/invitation), `document_id`, `role`, `invited_by_id`, `return_to`, `failed_attempts`, `expires_at`, `used_at` |
|
||||
| `documents` | `id` (UUID, in der URL), `owner_id`, `title`, `content` (JSON-Snapshot), `updated_by_id`, `content_updated_at` |
|
||||
| `document_updates` | `document_id`, `user_id`, `data` (ein Yjs-Update oder ein zusammengefasster Stand) |
|
||||
| `document_memberships` | `document_id`, `user_id` (eindeutig zusammen), `role` (viewer/editor), `source` (invite/link), `share_link_id` |
|
||||
| `share_links` | `document_id`, `role`, `token` (Klartext, damit der Besitzer den Link erneut kopieren kann), `created_by_id`, `revoked_at` |
|
||||
|
||||
Der Besitzer steht nur in `documents.owner_id`. Pro Dokument gibt es höchstens
|
||||
einen aktiven Link je Rolle.
|
||||
|
||||
### API
|
||||
|
||||
REST unter `/api`, JSON, Session-Cookie, CSRF-Token aus dem Meta-Tag;
|
||||
Link-Zugang über den Header `X-Share-Token`.
|
||||
|
||||
| Methode & Pfad | Zweck |
|
||||
| --- | --- |
|
||||
| `POST /api/auth/magic_link` | Link + Code anfordern `{email, return_to}` – Antwort immer gleich |
|
||||
| `POST /api/auth/code` | mit Code anmelden `{email, code}` |
|
||||
| `DELETE /api/auth/session` | abmelden |
|
||||
| `GET/PATCH /api/me` | angemeldeter User / Anzeigename |
|
||||
| `GET/POST /api/documents` | eigene + geteilte Dokumente / anlegen oder hochladen `{title, content}` |
|
||||
| `GET /api/documents/:id` | Metadaten, Rolle, JSON-Snapshot (`?content=0` ohne) |
|
||||
| `PATCH /api/documents/:id` | umbenennen `{title}` |
|
||||
| `DELETE /api/documents/:id` | Besitzer: löschen; sonst: aus der Liste entfernen |
|
||||
| `POST /api/documents/:id/join` | Link-Token einlösen `{token}` |
|
||||
| `GET /api/documents/:id/share` | Links, Mitglieder, offene Einladungen (Besitzer) |
|
||||
| `POST/DELETE /api/documents/:id/share/links[/:id]` | Link je Rolle neu erzeugen / widerrufen |
|
||||
| `POST /api/documents/:id/share/invitations` | einladen `{email, role}` |
|
||||
| `PATCH/DELETE /api/documents/:id/share/members/:user_id` | Rolle ändern / entfernen |
|
||||
| `GET/POST /auth/verify?token=` | Bestätigungsseite / Anmeldung aus dem Mail-Link |
|
||||
|
||||
### Konfiguration & Betrieb
|
||||
|
||||
Einstellungen stehen in einer **`.env`** im Projektverzeichnis (Gem `dotenv`;
|
||||
auch `.env.production` usw.). Vorlage: [`.env.example`](.env.example) –
|
||||
`cp .env.example .env` und ausfüllen. Echte Umgebungsvariablen haben Vorrang;
|
||||
`.env` wird nie committet.
|
||||
|
||||
| Variable | Wofür |
|
||||
| --- | --- |
|
||||
| `NUMOLUS_DATABASE_NAME`, `_HOST`, `_PORT`, `_USER`, `_PASSWORD` | PostgreSQL (Standardname `numolus`; ohne Host/User: lokaler Socket) |
|
||||
| `NUMOLUS_HOST` | Host in den Links der Mails |
|
||||
| `SECRET_KEY_BASE` | Produktion (`bin/rails secret`) |
|
||||
| `NUMOLUS_MAIL_FROM`, `SMTP_ADDRESS`, `SMTP_PORT`, `SMTP_USER_NAME`, `SMTP_PASSWORD` | Mailversand in Produktion |
|
||||
|
||||
In der Entwicklung werden Mails nicht verschickt, sondern unter
|
||||
<http://localhost:3000/letter_opener> angezeigt. Produktion braucht einen
|
||||
Rails-Server (Puma) mit PostgreSQL; `bin/jobs` bzw. `SOLID_QUEUE_IN_PUMA=1`
|
||||
für den Mailversand.
|
||||
|
||||
### Sicherheit
|
||||
|
||||
- Magic-Link-Token und Code nur als SHA-256 gespeichert, 15 Minuten gültig,
|
||||
einmal verwendbar; nach 5 falschen Codes ist der Link verbrannt.
|
||||
- Rate Limits: 20 Link-Anforderungen pro IP und Stunde (dazu 5 pro E-Mail),
|
||||
30 Code-Versuche pro IP in 15 Minuten.
|
||||
- `return_to` nur als Pfad derselben Seite; `404` statt `403`; UUIDs.
|
||||
- Cookies `HttpOnly`, `SameSite=Lax`, in Produktion `Secure`; CSRF-Schutz im
|
||||
REST-API.
|
||||
- Yjs-Updates max. 10 MB, JSON-Snapshots max. 20 MB und nur, wenn sie wie ein
|
||||
numolus-Dokument aussehen.
|
||||
|
||||
### Offen
|
||||
|
||||
- **Formeln bei gleichzeitigem Einfügen:** Tippt jemand eine Formel, während
|
||||
ein anderer eine Zeile einfügt, wird deren Bezug nicht angepasst (wie in
|
||||
Google Sheets). Später denkbar: Bezüge intern über IDs.
|
||||
- **Große Dokumente:** Jede fremde Änderung baut den ganzen Store neu auf;
|
||||
später nur die betroffene Tabelle.
|
||||
- **Makros aus fremder Hand** laufen im eigenen Browser, sobald man sie
|
||||
startet; eine Bestätigung für fremde Makros fehlt noch.
|
||||
- Papierkorb, Aufräumjobs (abgelaufene Magic Links, alte Sessions),
|
||||
„Überall abmelden“, Konto löschen / Datenexport, Offline-Kopie von
|
||||
Server-Dokumenten (`y-indexeddb`), Backups und Monitoring.
|
||||
- Datenschutzerklärung (gespeichert werden E-Mail, Anzeigename, IP und
|
||||
User-Agent je Session).
|
||||
|
||||
## Vorbefülltes Dokument (Datei › Muster)
|
||||
|
||||
@@ -1148,7 +1396,8 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
|
||||
## Tests
|
||||
|
||||
`npm run test` — Vitest, 570 Tests:
|
||||
`npm run test` — Vitest (Engine, Store, IO, Yjs-Abbildung);
|
||||
`bin/rails test` — Minitest (Models, REST-API, Anmeldung, Teilen, DocumentChannel):
|
||||
|
||||
- **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^`
|
||||
rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`,
|
||||
@@ -1513,10 +1762,21 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
app/
|
||||
models/ # User, Session, MagicLink, Document, DocumentUpdate, DocumentMembership, ShareLink
|
||||
controllers/ # SpaController (liefert die App), AuthController (Mail-Link), api/* (REST)
|
||||
channels/ # DocumentChannel (Live-Bearbeitung), ApplicationCable::Connection
|
||||
mailers/, views/ # AuthMailer (Anmeldung, Einladung), Bestätigungsseite, SPA-Seite
|
||||
config/, db/ # Rails-Konfiguration, Migrationen (inkl. Solid Queue/Cache/Cable)
|
||||
test/ # Minitest: Models, API, Channel
|
||||
.env.example # Vorlage für Datenbank, Host, SMTP
|
||||
app/frontend/
|
||||
entrypoints/ # application.ts – Einstieg (vite_rails)
|
||||
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)
|
||||
store/ # Dokument-Store (v2 Canvas-Modell), Formatierung, library.ts (lokal/Server), session.ts (Konto)
|
||||
collab/ # Live-Bearbeitung: ydoc.ts (Yjs-Abbildung), session.ts (ActionCable, Undo, Awareness)
|
||||
io/ # csv / json (Eigenbau, inkl. v1→v2), num (ZIP via fflate, lazy), xlsx (SheetJS, lazy),
|
||||
# library.ts (IndexedDB), api.ts (REST-Client)
|
||||
components/ # AppToolbar, SettingsPanel, FormulaBar, SheetCanvas, CanvasObject,
|
||||
# TableGrid, ChartView, TextBox, ButtonView, MacrosModal, SheetTabs,
|
||||
# StatusBar, TheLogo, …
|
||||
|
||||
Reference in New Issue
Block a user