Pure moves, no behaviour change: the storage layer now has users, apikeys, thoughts, tasklists, documents, folders, tags, lists, teams and system files next to sqlite.go (store, schema, migrations); the API has handlers_*.go per area plus realtime.go and alarms.go next to server.go (server, router, middleware). Declarations were moved verbatim with an AST-based tool; imports pruned with goimports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 KiB
Bekannte Schwachstellen, Risiken & technische Schulden (Flaws)
Stand: Code-Review des Branches dev (Oktober 2026). Jeder Eintrag wurde gegen den Quellcode geprüft; die Fundstellen sind angegeben. Einträge ohne Live-Test sind als nicht live verifiziert markiert.
Schweregrade: Hoch (Datenabfluss oder Rechteumgehung möglich), Mittel (Fehlfunktion oder Schutz greift nicht), Niedrig (Wartbarkeit, Komfort, Hygiene).
Übersicht
| ID | Bereich | Kurzbeschreibung | Schwere |
|---|---|---|---|
| FLAW-01 | Realtime | BroadcastToAll sendet Aufgaben- und Listendaten an alle Verbindungen |
Behoben |
| FLAW-02 | Realtime | Behoben | |
| FLAW-03 | Aufgaben | ReorderTasks ändert beliebige Aufgaben per ID (IDOR) |
Behoben |
| FLAW-04 | Auth | JWT_SECRET |
Behoben |
| FLAW-05 | Benachrichtigungen | Behoben | |
| FLAW-06 | Aufgaben | teamId an weitere Gruppen freigeben |
Behoben |
| FLAW-07 | Auth | Niedrig | |
| FLAW-08 | Auth | token_version noch is_active |
Behoben |
| FLAW-09 | Auth | Kein Rate-Limit, keine Sperre bei Fehlversuchen, Mindestpasswort 4 Zeichen | Mittel |
| FLAW-10 | Freigaben | sharing_mode wird bei Freigaben nicht durchgesetzt; Benutzerverzeichnis leakt alle Konten |
Behoben |
| FLAW-11 | Workspace | ON CONFLICT(id)) prüft das Dokument nicht |
Behoben |
| FLAW-12 | Benachrichtigungen | notify_before_minutes wirkungslos; Alarm feuert nach Umplanung nie wieder |
Teilweise behoben |
| FLAW-13 | Datenbank | Keine Schema-Versionierung; Migrationsfehler werden verschluckt; migrations/001_init.sql ist toter Code |
Mittel |
| FLAW-14 | Backend | Fehler von Schreiboperationen werden häufig ignoriert (_, _ =) |
Mittel |
| FLAW-15 | Realtime | WriteTimeout von 30 s beendet SSE-Streams |
Behoben |
| FLAW-16 | Setup | /auth/setup ist bis zum ersten Konto offen; Demo-Modus legt admin/admin an |
Mittel |
| FLAW-17 | Workspace | Behoben | |
| FLAW-18 | Suche | LIKE ohne Escaping, keine FTS |
Behoben |
| FLAW-19 | Qualität | Frontend-Tests nur für den Outliner, lint ist ein Platzhalter |
Niedrig |
| FLAW-20 | Qualität | Niedrig | |
| FLAW-21 | Repo | LICENSE fehlt, obwohl README MIT angibt |
Niedrig |
| FLAW-22 | Netzwerk | Token in der URL (WS/SSE), CORS *, CheckOrigin immer true |
Niedrig |
| FLAW-23 | SQLite | Single-Writer-Engpass bei vielen gleichzeitigen Schreibern | Niedrig |
| FLAW-24 | Gruppen | Behoben | |
| FLAW-25 | Workspace | Behoben | |
| FLAW-26 | Workspace | Behoben |
Details
FLAW-01: Broadcast an alle Verbindungen (Datenabfluss)
- Status: Behoben.
BroadcastToAllexistiert nicht mehr. Aufgabenereignisse gehen an Eigentümer und Benutzer der Liste (GetTaskListUserIDs), Listenereignisse tragen nur noch die ID, Ordnerereignisse gehen an Benutzer bzw. Ordnerbeteiligte (GetSharedFolderUserIDs). Test:TestTaskEventsOnlyReachAuthorizedUsers. - Fundstellen:
internal/ws/hub.goBroadcastToAll; Aufrufer ininternal/api/server.go:handleUpdateThought(TASK_UPDATED),handleDeleteThought(TASK_DELETED),handleReorderTasks(TASK_REORDERED),handleUpdateTaskList,handleCreateTaskListShare,handleDeleteTaskListShare(TASK_LIST_UPDATED),handleDeleteTaskList. - Problem:
BroadcastToAllschreibt anallClientsundsseClients. Dazu zählen auch Verbindungen mituserID = "anonymous", dennhandleWebSocketundhandleSSEliegen außerhalb der Auth-Gruppe und lassen jede Verbindung zu. Der vollständige Aufgabeninhalt (Titel, Notizen, Unteraufgaben) und die Listen samt Mitgliedern gehen so an jeden, der/api/v1/eventsöffnet. - Maßnahme: Ereignisse an die berechtigten Nutzer adressieren (wie bei
DOC_*überGetAuthorizedDocUsers), unauthentifizierte Verbindungen ablehnen.
FLAW-02: Räume ohne Berechtigungsprüfung
- Status: Behoben.
/wsund/eventsverlangen ein gültiges Token (sonst401). Jeder Raumbeitritt läuft überHub.SetRoomAuthorizer→Server.canJoinRoom(Notiz bzw. Liste muss lesbar sein)./events/roomsliegt hinter der Anmeldung und akzeptiert nur die eigenesseId. Tests:TestRealtimeEndpointsRequireAuth,TestSSERoomJoinRequiresAccess,TestRoomJoinsRequireAuthorization. - Fundstellen:
Hub.readPump(JOIN_ROOM),handleSSERooms(Route/api/v1/events/roomsliegt außerhalb der Auth-Gruppe),Hub.Register(Raum aus?listId=/?spaceId=). - Problem: Wer eine Raum-ID kennt (
doc:<id>, Listen-ID), tritt bei und erhält Inhalte wieDOC_BLOCKS_PATCHEDoderITEM_*. Raum-IDs tauchen in Ereignissen auf und sind nicht geheim. Für den SSE-Beitritt reicht diesseId, die der Server selbst im Stream mitsendet. - Maßnahme: Beitritt nur nach Zugriffsprüfung (
GetDocumentByID,GetListByID);/events/roomshinterauthMiddleware.
FLAW-03: ReorderTasks ohne Zugriffsprüfung (IDOR)
- Status: Behoben.
handleReorderTaskslehnt die ganze Anfrage ab, sobald eine Aufgabe nicht zugreifbar (404) oder schreibgeschützt (403) ist. Test:TestReorderRejectsForeignTasks. - Fundstelle:
Store.ReorderTasks(UPDATE thoughts SET sort_order … WHERE id = ?) undhandleReorderTasks. - Problem: Die SQL-Anweisung ist weder auf
user_idnoch auf die Liste eingeschränkt. Jeder angemeldete Nutzer kannsortOrder,kanbanStatusund damitisCompletedfremder Aufgaben ändern, sofern er die ID kennt. Die Leser-Prüfung für Aufgabenlisten überspringt Aufgaben, auf die der Nutzer gar keinen Zugriff hat (GetThoughtByIDliefert dann einen Fehler), und schützt davor nicht. - Maßnahme: Jede ID gegen
accessibleTaskListIDsQuerybzw.user_idprüfen und bei Verstoß ablehnen.
FLAW-04: Fallback-JWT-Secret
- Status: Behoben. Fehlt
JWT_SECREToder ist es ein bekannter Platzhalter, erzeugt der Server 32 Zufallsbytes und speichert sie alsjwt.secret(Rechte 0600) im Datenverzeichnis; ein Hinweis steht im Log. Test:internal/config/config_test.go. - Fundstelle:
internal/config/config.go:getEnv("JWT_SECRET", "mindsift-default-selfhosted-jwt-secret-key-2026");docker-compose.ymlund.env.exampleliefern ebenfalls einen Platzhalter. - Problem: Wer
JWT_SECRETnicht setzt (oder den Platzhalter behält), läuft mit einem öffentlich bekannten Schlüssel. Damit lassen sich gültige Tokens für jede Benutzer-ID fälschen.install.sherzeugt zwar ein Secret, andere Installationswege nicht. - Maßnahme: Start verweigern oder Secret beim ersten Start erzeugen und in
/dataablegen.
FLAW-05: SSRF über Alarm-URLs
- Status: Behoben. netguard.go: URLs werden beim Speichern und vor jedem Versand geprüft (https Pflicht, Discord nur
discord.com/api/webhooks/). Der HTTP-Client löst den Host selbst auf und verbindet nur zu erlaubten IPs: localhost, Link-Local/Cloud-Metadaten sind immer gesperrt, Heimnetz-Bereiche (RFC 1918, ULA, CGNAT/Tailscale) und http nur mit dem Admin-Schalter „Alarm-Webhooks ins lokale Netz erlauben“. Keine Umgebungs-Proxys, höchstens 3 Weiterleitungen. - Fundstellen:
handleUpdateUserSettings(keine Validierung),sendDiscordNotification,sendCustomWebhookNotification. - Problem: Jeder Nutzer kann beliebige URLs hinterlegen. Der Server sendet POST-Anfragen dorthin, auch an interne Adressen (
http://169.254.169.254, Docker-Netz,localhost). ÜberhandleTestUserAlarmlässt sich das sofort auslösen. - Maßnahme: Nur
https, Discord-Host prüfen, private/Loopback-Adressen beim Verbinden sperren.
FLAW-06: Editor kann Aufgabenliste weiterteilen
- Status: Behoben. Das Feld
task_lists.team_idist entfernt; Freigaben gibt es nur noch übertask_list_shares, und die legt nur der Eigentümer an. - Fundstelle:
Store.UpdateTaskList(teamID-Zweig ruftAddTaskListShare);handleUpdateTaskListprüft nur, dass der Nutzer kein Leser ist. - Problem:
POST /task-lists/{id}/sharesist Eigentümern vorbehalten. ÜberPUT /task-lists/{id}mitteamIdkann aber jeder Editor die Liste einer weiteren Gruppe zuweisen (mit Schreibrecht) und die Zuweisung der bisherigen Gruppe aufheben. - Maßnahme:
teamId-Änderungen nur für den Eigentümer; das Legacy-Feldtask_lists.team_idzugunsten vontask_list_sharesabschaffen.
FLAW-07: API-Key-Scopes ohne Wirkung
- Teilweise behoben: API-Keys dürfen nur noch
POST /inboxundPOST /webhooks/ingestnutzen (apiKeyAllowedinserver.go), alles andere gibt403. Auswählbare Scopes (Triage, Aufgaben, Listen) stehen noch aus. - Fundstellen: Tabelle
api_keys.scopes(Standardingest,read),authMiddleware. - Problem: Ein Key mit Präfix
mindsift_sec_authentifiziert als der Benutzer, mit allen Rechten: alle/api/v1-Routen, bei Admins auch/admin/*, außerdem das Anlegen weiterer Keys. Für Webhooks (nur Aufgaben anlegen) ist das überdimensioniert. - Maßnahme: Scopes auswerten; Keys standardmäßig nur auf
/inboxund/webhooks/ingestbeschränken.
FLAW-08: Webhook-Ingest umgeht Sitzungsprüfung
- Status: Behoben. Middleware und Webhook-Ingest nutzen dieselbe Prüfung (
authenticateToken): Sperrliste,token_version, aktives Konto. Test:TestAPIKeysOnlyDeliverData. - Fundstelle:
handleWebhookIngest, JWT-Zweig. - Problem: Das Token wird nur kryptografisch validiert.
IsTokenRevoked,token_versionundis_activewerden nicht geprüft; ein ausgeloggtes oder deaktiviertes Konto kann weiter Aufgaben anlegen (bis Token-Ablauf nach 30 Tagen). - Maßnahme: Dieselbe Prüfkette wie in
authMiddlewareverwenden (gemeinsame Funktion statt Kopien; siehe auchauthenticateUserFromReq).
FLAW-09: Kein Schutz gegen Brute-Force
- Fundstellen:
handleLogin,handleRegister,handleFirstBootSetup. - Problem: Weder Rate-Limit noch Sperre;
middleware.RealIPvertrautX-Forwarded-Forungeprüft. Mindestpasswortlänge ist 4 Zeichen. Der Login-Pfad ist so über das Internet raten- und durchprobierbar. - Maßnahme: Rate-Limit je IP und Konto, längeres Mindestpasswort.
FLAW-10: sharing_mode wird nicht durchgesetzt
- Status: Behoben. Modi
private/organizations/open, zentral durchgesetzt überStore.CanSee/CanShareWithin Verzeichnis, allen Freigaben und Gruppen; Rechte werden bei Sichtbarkeitsverlust bereinigt (sharing-and-permissions.md). Test:internal/storage/visibility_test.go. - Fundstellen:
Store.GetDiscoverableUsers,Store.GetCollaborators; Share-Handler inserver.go. - Problem:
- Der Modus beeinflusst nur
GetDiscoverableUsers. Inteams_onlyliefert die Funktion trotzdem alle Benutzer (if … mode == "open" || mode == "teams_only" { return GetAllUsers() }). GET /collaboratorsignoriert den Modus komplett und liefert alle aktiven Konten mit E-Mail-Adresse.- Keine der Share-Funktionen prüft den Modus.
strict_privateundteams_onlyverhindern Freigaben nicht, wenn man die Ziel-ID kennt.
- Der Modus beeinflusst nur
- Maßnahme: Eine zentrale Funktion
canShareWith(owner, target)und in allen Share-Handlern verwenden; Verzeichnisse darauf einschränken.
FLAW-11: Block-Upsert prüft das Dokument nicht
- Status: Behoben. Alle Blockänderungen laufen über
Store.applyDocChange: Einfügen mit einer Block-ID aus einer anderen Notiz wird abgelehnt (exists), und der Upsert ändert nur Zeilen derselben Notiz (WHERE blocks.document_id = excluded.document_id). Test:TestInsertCannotReuseBlockIDOfAnotherNote. - Fundstelle:
Store.PatchDocumentBlocks,INSERT INTO blocks … ON CONFLICT(id) DO UPDATE. - Problem:
blocks.idist global eindeutig. Existiert die ID bereits in einem anderen Dokument, überschreibt der Konflikt-Zweig dessen Inhalt, ohnedocument_idzu prüfen. Voraussetzung: Schreibrecht im eigenen Dokument und Kenntnis einer fremden Block-ID. - Maßnahme:
DO UPDATE … WHERE blocks.document_id = excluded.document_id; fremde IDs ablehnen.
FLAW-12: Alarm-Logik
- Teilweise behoben: Fälligkeiten haben eine optionale Uhrzeit; der Alarm kommt zur Uhrzeit (ganztägig 9:00) minus
notify_before_minutes, in der ZeitzoneTZ(Standard Europe/Berlin, Zeitzonendaten eingebettet). Neue Fälligkeit oder Wiederöffnen setztalarm_sentzurück. Tests:internal/domain/due_test.go,internal/api/alarm_test.go,TestAlarmResetsOnNewDueDateOrReopen. Offen bleibt der letzte Punkt (kein Backoff). - Fundstellen:
startAlarmTicker,GetDueTasksForAlarm,UpdateThoughtDetails,UserSettingsView. - Probleme:
notify_before_minutes(Einstellungen: „15 Minuten vorher“ usw.) wird nirgends gelesen. Fälligkeiten haben nur ein Datum (YYYY-MM-DD), eine Vorlaufzeit ist damit nicht abbildbar.- Verglichen wird mit
time.Now()in der Zeitzone des Containers (ohneTZalso UTC). alarm_sentwird nie zurückgesetzt. Wird die Fälligkeit nach einem Alarm verschoben, gibt es keinen neuen.- Schlagen alle Kanäle fehl, wird jede Minute erneut gesendet (kein Backoff, keine Dead-Letter-Queue).
- Maßnahme: Fälligkeit mit Uhrzeit speichern,
alarm_sentbei Änderung vondue_datezurücksetzen, Wiederholungen begrenzen.
FLAW-13: Migrationen
- Teilweise behoben: Das Schema hat eine Version (
PRAGMA user_version,schemaVersioninsqlite.go), auf die künftige Migrationen aufsetzen können. Die übrigen Punkte unten gelten weiter. - Fundstellen:
Store.migrateininternal/storage/sqlite.go,migrations/001_init.sql. - Probleme:
- Es gibt keine Versionstabelle. Alle
ALTER TABLE-Anweisungen laufen bei jedem Start erneut; Fehler werden pauschal ignoriert (_, _ = s.db.Exec(q)), echte Fehler fallen nicht auf. - Mehrere
UPDATE/DROP TABLE-Bereinigungen laufen bei jedem Start. migrations/001_init.sqlwird nur in das Image kopiert, aber nirgends gelesen, und ist gegenüber dem tatsächlichen Schema veraltet.
- Es gibt keine Versionstabelle. Alle
- Maßnahme:
schema_migrations-Tabelle mit nummerierten, einmalig ausgeführten Schritten; die SQL-Datei entweder einbetten oder entfernen.
FLAW-14: Verschluckte Fehler
- Fundstellen: u. a.
PatchDocumentBlocks(Upserts/Deletes),AddTaskListShare,RemoveTaskListShare, viele_, _ = s.db.Exec. - Problem: Scheitert ein Schreibzugriff (z. B.
SQLITE_BUSY), antwortet die API trotzdem mit Erfolg und broadcastet den Stand, der nie gespeichert wurde. Fehler lassen sich nur schwer nachvollziehen. - Beispiel (behoben): Das Lösen der Verknüpfung zwischen Notiz-Block und Aufgabe („Verknüpfung aufheben“, Löschen eines verknüpften Blocks) schrieb in nicht existierende Spalten
thoughts.doc_id/block_id. Der Fehler wurde verschluckt, die Funktion war nie wirksam. Korrigiert; im neuen Sync-Weg gleichtReconcileDocTaskLinksHäkchen und gelöschte Blöcke ab. - Maßnahme: Mehrstufige Änderungen in Transaktionen bündeln und Fehler zurückgeben.
FLAW-15: WriteTimeout und SSE
- Status: Behoben.
ServeSSEhebt die Schreibfrist für die eigene Antwort auf (http.ResponseController.SetWriteDeadline). Test mit 1-s-Timeout:TestSSEStreamOutlivesWriteTimeout. Der Test hat den Abbruch im alten Code bestätigt. - Fundstelle:
cmd/server/main.goWriteTimeout: 30 * time.Second. - Problem: Go beendet nach diesem Zeitraum das Schreiben der Antwort. Für den SSE-Fallback (
ServeSSE) bedeutet das voraussichtlich einen Verbindungsabbruch alle 30 s. WebSockets sind durch Hijacking nicht betroffen. Der Client verbindet sich neu, daher fällt es kaum auf. - Maßnahme: Im SSE-Handler
http.ResponseController.SetWriteDeadline(time.Time{})setzen oderWriteTimeoutfür diese Route aufheben.
FLAW-16: Ersteinrichtung und Demo-Modus
- Fundstellen:
handleFirstBootSetup,Store.EnsureDemoAccounts,config.Load(MINDA_DEMO,MINDSIFT_DEMO,DEMO_MODE). - Probleme: Solange kein Konto existiert, kann jeder
POST /auth/setupaufrufen und wird Administrator. Mit gesetztem Demo-Flag entstehen Kontenadmin/adminunduser/user; versehentlich auf einer öffentlichen Instanz aktiv wäre das eine Komplettübernahme. - Maßnahme: Setup per einmaligem Token aus dem Server-Log absichern; Demo-Modus nur zusammen mit expliziter Warnung/Bestätigung.
FLAW-17: Schreibgeschützte Dokumentfreigaben
- Status: Behoben. Zwangsmigration entfernt, Backend prüft Bearbeiter-Rechte (
DocRole,/docs/{id}/sync), der Editor speichert über die Sync-Engine, und der Freigabe-Dialog bietet „Bearbeiten“ an. Browser-Test:e2e/share.e2e.mjs. - Fundstellen: Migration in
Store.migrate(UPDATE doc_shares SET permission = 'read' …, ebensodoc_team_shares,workspace_folder_shares),PatchDocumentBlocks/UpdateDocument(nur Eigentümer). - Hinweis: Das ist offenbar gewollt: Nur der Eigentümer schreibt in ein Dokument, Empfänger lesen. Es gibt daher kein kollaboratives Bearbeiten. Das Schema (
CHECK(permission IN ('read','write'))), die Zweige fürwriteim Code und frühere Dokumentation suggerieren etwas anderes. Es gibt weder Sperren noch Konfliktauflösung, noch Cursor-Presence. - Maßnahme: Entweder Schreibfreigaben mit Konfliktbehandlung ergänzen oder den toten Code und das Schema bereinigen.
FLAW-18: Suche
- Status: Behoben. SQLite FTS5-Volltexttabellen mit BM25-Ranking (
thoughts_fts,documents_fts,list_items_fts), Trigger zur automatischen Synchronisation und initialem Backfill beim Start. Sicheres Query-Escaping, Präfixsuche. Strikte Berechtigungsprüfung überaccessibleTaskListIDsQuery,DocRole(eigene, geteilte Notizen, über Ordner gemountete Notizen) und zugängliche Listen. Rückwärtskompatible API-Antwort mit Snippets (<mark>) und Frontend-Integration überCommandPalette(Cmd+K/Ctrl+K//). - Fundstellen:
internal/storage/search.go,internal/api/server.go,frontend/src/components/CommandPalette.tsx.
FLAW-19: Geringe Frontend-Testabdeckung
- Stand: Vitest mit jsdom und Testing Library ist eingerichtet (
npm run test:run) und läuft beim Image-Build. Getestet sind bisher nur Outliner-Funktionen (Backspace-Zusammenführen, Cursor-Position beim Klicken). - Problem: Aufgaben, Listen, Freigabe-Dialoge und Berechtigungsanzeigen sind ungetestet.
"lint"infrontend/package.jsonist weiterhin ein Platzhalter (echo). - Maßnahme: Tests für kritische Pfade ergänzen, ESLint als echtes
lint-Skript.
FLAW-20: Dateigröße und Duplikate
- Teilweise behoben:
sqlite.goundserver.gosind nach Bereichen in eigene Dateien aufgeteilt (reine Verschiebung, Verhalten unverändert; siehe architecture.md). Offen: große Frontend-Dateien und die doppelte Zugriffsabfrage. internal/storage/sqlite.go(≈ 5.600 Zeilen),internal/api/server.go(≈ 3.000),WorkspaceView.tsx(≈ 2.300),TasksView.tsx(≈ 2.500),api.ts(≈ 1.500).- Die Zugriffsabfrage für Aufgabenlisten steht dreimal fast identisch im Code (
accessibleTaskListIDsQuery,GetTaskLists,GetTaskListByID). Dokumentzugriff ist ähnlich verteilt. Änderungen an Berechtigungen müssen an mehreren Stellen erfolgen und werden leicht vergessen. - Maßnahme: In Teilmodule zerlegen (Auth, Dokumente, Aufgaben, Listen, Teams), Zugriffsprüfung zentralisieren.
FLAW-21: Lizenzdatei fehlt
- README und Badge nennen die MIT-Lizenz und verlinken auf
LICENSE; die Datei existiert nicht im Repository.
FLAW-22: Netzwerk-Details
authenticateUserFromReqakzeptiert das Token als?token=(nötig fürWebSocket/EventSource, die keine Header setzen). Tokens landen dadurch in Proxy- und Zugriffslogs.AllowedOrigins: ["*"](Auth läuft überAuthorization-Header, nicht über Cookies, das Risiko ist daher begrenzt) undCheckOriginliefert immertrue.- Ein einmal verbundener WebSocket/SSE-Stream wird bei Logout oder Token-Widerruf nicht getrennt.
- Maßnahme: Kurzlebiges, einmaliges Stream-Ticket statt Langzeit-JWT in der URL; Origin gegen
BASE_URLprüfen.
FLAW-23: SQLite-Schreibengpass
- Eine SQLite-Datei (WAL,
busy_timeout, bis zu 8 offene Verbindungen) erlaubt nur einen gleichzeitigen Schreiber. Für Familien und kleine Teams unkritisch; bei vielen aktiven Editoren sindSQLITE_BUSY-Fehler möglich, die wegen FLAW-14 stumm bleiben.
FLAW-24: Organisations-Endpunkte ohne Berechtigungsprüfung
- Status: Behoben. Die alten
/organizations-Endpunkte sind entfernt; Organisationen verwaltet nur der Server-Admin über/admin/organizations. - Fundstellen:
handleDeleteOrganization,handleGetOrgTeams,handleCreateOrgTeam(undStore.DeleteOrganization) inserver.gobzw.sqlite.go. - Problem: Diese Handler prüfen weder Eigentümer noch Mitgliedschaft. Jeder angemeldete Nutzer kann mit bekannter ID eine fremde Organisation löschen, deren Gruppen auflisten oder dort Gruppen anlegen. Gruppenbezogene Endpunkte (
/teams/{id}…) prüfen dagegen korrekt auf Gruppen-Admin. Das Datenmodell kennt Organisationen nur noch rudimentär; im UI werden Gruppen direkt überPOST /teamsangelegt (orgIdStandarddefault). - Maßnahme: Organisationen entweder konsequent absichern (Eigentümer/Server-Admin) oder entfernen.
FLAW-25: Teilweise ausgeführte Struktur-Aktionen an gesperrten Bullets
- Status: Behoben. Fremd gesperrte Bullets sind im Editor schreibgeschützt und zeigen, wer sie bearbeitet. Löschen, Zusammenführen, Ein-/Ausrücken und Ziehen werden nicht gestartet, wenn ein beteiligter Bullet (oder einer im Ast) gesperrt ist; stattdessen erscheint ein Hinweis. Ebenso für „Übernehmen“/„Verwerfen“ und Rückgängig. Browser-Test:
e2e/lock.e2e.mjs. - Fundstellen:
handleMergeWithPreviousundhandleIndentBlockinOutlinerTree.tsx; Sperrregeln ininternal/collab(Verschieben/Löschen gesperrter Blöcke wird abgelehnt). - Problem: Eine Editor-Aktion besteht oft aus mehreren Operationen. Ist einer der beteiligten Bullets von jemand anderem gesperrt, lehnt der Server nur diese Operation ab. Beispiel aus dem Browser-Test: Backspace am Zeilenanfang hängt den Text oben an (angewendet), das Löschen des unteren Bullets wird abgelehnt – der Text steht dann doppelt da. Einrücken eines fremd gesperrten Bullets wird ohne Hinweis zurückgenommen. Es geht kein Text verloren.
- Maßnahme (Phase 3): Gesperrte Bullets im Editor anzeigen und Struktur-Aktionen, die sie berühren, gar nicht erst ausführen.
FLAW-26: Fokus-Verzögerung nach Enter
- Status: Behoben. Neu eingehängte Bullets bekommen den Fokus sofort beim Einhängen. Browser-Test: „Sofort nach Enter Getipptes landet im neuen Bullet“.
- Fundstelle:
focusBlockInputinOutlinerTree.tsxsetzt den Fokus erst im nächsten Animation-Frame. - Problem: Zeichen, die innerhalb dieses Frames getippt werden (automatisiert oder sehr schnelles Tippen), landen noch im vorherigen Bullet. Bei fremd gesperrtem Bullet entsteht daraus eine Konfliktkopie.
- Maßnahme: Fokus nach dem Anlegen synchron setzen (
useLayoutEffect).
FLAW-27: Grenzen nach der Verdichtung
- Status: Bekannt, bewusst so.
- Fundstellen:
CompactDocsininternal/storage/docsync.go,reviveAsNewBlockininternal/collab/ops.go. - Verhalten: Nach 30 Tagen sind gelöschte Blöcke endgültig weg. Rückgängig für ein so altes Löschen ist nicht mehr möglich (Hinweis im Editor). Text, der wochenlang offline in einem inzwischen endgültig gelöschten Block getippt wurde, kommt als neuer Block am Ende der Notiz zurück, nicht an der alten Stelle. Wer eine Operation erst nach mehr als 90 Tagen erneut sendet (Antwort ging verloren, Gerät so lange offline), wird nicht mehr als Wiederholung erkannt; Textänderungen erzeugen dann höchstens eine Konfliktkopie, kein stilles Überschreiben.
Aus der früheren Fassung entfernt
Die frühere flaws.md führte als „gelöst“, was im Code nicht existiert: In-Memory-DocRooms, clientId-Echo-Schutz, Cursor-Presence, 1,5-s-Debounce-Flush, internal/ws/docroom.go, frontend/src/lib/useDocumentSync.ts und Yjs/CRDT. Der Code enthält nur noch internal/ws/hub.go (Räume, Benutzer-Kanäle, SSE-Fallback). Die CRDT-Tabellen werden beim Start gelöscht (DROP TABLE document_crdt_*). Dokumente werden per REST gespeichert (PATCH /documents/{id}/blocks) und als DOC_BLOCKS_PATCHED verteilt. Die frühere Aussage zur Token-Versionierung (token_version, revoked_tokens) trifft dagegen zu und ist in sharing-and-permissions.md und architecture.md beschrieben.