MindSift now speaks MCP (Streamable HTTP, stateless JSON-RPC) at POST /api/v1/mcp so Claude Code, Codex, Claude Desktop, Cursor and custom agents can read, create and complete tasks and keep shared lists. API keys get the scopes tasks:read/write and lists:read/write; tools/list only shows what the key may call and every tool checks its scope. Task and list write paths are shared helpers (updateTask, createTask, addListItem, setListItemDone) used by REST and MCP alike. Settings: scope selection and the SectionPicker (import, restore) use ToggleSwitch rows instead of checkbox-like buttons; new MCP section with connection snippets for Claude Code, Codex and JSON configs. Docs, README, help and changelog updated; v0.28.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
27 KiB
Architektur
Überblick
Browser (React SPA, PWA-Manifest)
│ REST /api/v1/* Authorization: Bearer <JWT | mindsift_sec_…>
│ WebSocket /api/v1/ws Fallback: SSE /api/v1/events
▼
Go-Server (chi) ── internal/api Handler, Middleware, Alarm-Ticker
│ internal/ws Hub (Räume, Benutzer-Kanäle, SSE)
│ internal/auth JWT, bcrypt, API-Key-Hash
▼ internal/config
SQLite (modernc.org/sqlite, WAL) ── internal/storage
Das Frontend liegt per go:embed im Binary (cmd/server/main.go). Unbekannte Pfade liefern index.html (SPA-Routing); index.html, sw.js und manifest.json werden mit no-store ausgeliefert.
Stack
| Schicht | Technik |
|---|---|
| Backend | Go 1.23, go-chi/chi v5, go-chi/cors, gorilla/websocket, golang-jwt/jwt v5, x/crypto/bcrypt |
| Datenbank | SQLite über modernc.org/sqlite (reines Go, kein CGO), WAL, foreign_keys=ON |
| Frontend | React 18, TypeScript, Vite 5, Tailwind 3, framer-motion (Triage-Karten), lucide-react, prismjs (Code-Blöcke) |
| Auslieferung | Mehrstufiges Dockerfile (Node 22 → Go 1.23 → Alpine 3.21), Image git.dsit.me/sandrodach/mindsift |
Verzeichnisse
| Pfad | Inhalt |
|---|---|
cmd/server/ |
main.go, eingebettetes dist/ |
internal/api/ |
server.go (Server, Router mit allen Routen, Middleware, JSON-Helfer); Handler nach Bereich in handlers_*.go (auth, account, thoughts, documents, folders, lists, tasklists, teams, admin, integrations), realtime.go (WebSocket/SSE), alarms.go (Ticker und Versand), docsync.go, mail.go, netguard.go, password_reset.go |
internal/storage/ |
sqlite.go (Store, Schema, Migrationen); Datenlogik nach Bereich: users.go, apikeys.go, thoughts.go (inkl. Alarme), tasklists.go, documents.go, folders.go, tags.go, lists.go, teams.go, system.go (Einstellungen, Statistik, Export), dazu visibility.go, docsync.go, search.go, trash.go, backlinks.go, recurrence.go u. a. |
internal/domain/ |
Strukturen (models.go), ID-Erzeugung, Tag-Extraktion |
internal/ws/ |
hub.go |
internal/collab/ |
Kern der Notiz-Synchronisation ohne Datenbank: Blockbaum, Operationen und Konfliktregeln, Sortierschlüssel, Leases, Long-Poll-Benachrichtigung |
internal/auth/ |
Tokens und Passwörter |
internal/config/ |
Umgebungsvariablen |
frontend/src/features/ |
triage, workspace, lists, teams, settings, capture, auth, help |
frontend/src/lib/ |
api.ts (REST-Client, Transport-Umschaltung WS/SSE), types.ts, markdown.tsx, theme.ts |
frontend/src/lib/docSync/ |
Notiz-Synchronisation im Browser: Portierung von internal/collab (model.ts, orderKey.ts), Vergleich Editor-Liste → Operationen (diff.ts), engine.ts, IndexedDB (persist.ts), HTTP (transport.ts) |
e2e/ |
Browser-Tests mit Playwright gegen einen echten Server (e2e/run.sh) |
frontend/src/themes/ |
CSS pro Theme |
Start und Laufzeit
config.Load()liest die Umgebung (siehe unten).storage.NewStore()öffnet SQLite (busy_timeout, WAL, max. 8 Verbindungen) und führtmigrate()aus.- SMTP-Werte aus der Umgebung werden in
system_settingsübernommen, sofern dort noch nichts steht. - Mit Demo-Flag legt
EnsureDemoAccountsdie Kontenadmin/adminunduser/useran. api.NewServerstartet den Alarm-Ticker (jede Minute), die Verdichtung des Notiz-Logs (beim Start, dann alle 6 Stunden) und baut den Router. Server-Timeouts: Lesen 15 s, Schreiben 30 s, Leerlauf 60 s. Beim Beenden wird sauber heruntergefahren.
Konfiguration
| Variable | Standard | Bedeutung |
|---|---|---|
PORT |
8080 |
HTTP-Port |
DATA_DIR |
./data (Container: /data) |
Verzeichnis der Datenbank |
DATABASE_URL |
leer | sqlite:///pfad/datei.db oder Pfad; ohne Angabe DATA_DIR/mindsift.db. Postgres-URLs werden nicht unterstützt (siehe Hinweis) |
JWT_SECRET |
zufällig erzeugt, gespeichert in $DATA_DIR/jwt.secret |
Signaturschlüssel der Tokens; bekannte Platzhalter werden ignoriert |
TRUSTED_PROXIES |
private |
Proxys, deren X-Forwarded-For/X-Real-IP geglaubt wird (private, all, none, CIDR-Liste); internal/api/clientip.go |
LOGIN_PROTECTION |
true |
Login-Sperre nach Fehlversuchen und Limits für Registrierung und „Passwort vergessen“ (internal/api/loginguard.go), false = aus |
BASE_URL |
http://localhost:<PORT> |
Basis-URL der Instanz; explizit gesetzt Voraussetzung für das Passwort-Zurücksetzen per Mail |
TZ |
Europe/Berlin |
Zeitzone für Fälligkeiten und Alarme; die Zeitzonendaten sind ins Binary eingebettet (time/tzdata) |
REGISTRATION_MODE, REGISTRATION_DOMAINS |
leer | Registrierung ohne Einladung (closed, approval, domain, domain+approval, open) und erlaubte E-Mail-Endungen. Gesetzt gewinnen sie über system_settings und sind im Admin-UI gesperrt (registrationPolicy in internal/api/registration.go). Wartende Konten: is_active = 0 plus Zeile in pending_registrations (Migration 10) |
REGISTRATION_VERIFY_EMAIL |
leer | E-Mail-Bestätigung neuer Konten ohne Einladung (nur mit SMTP und BASE_URL). Token wie beim Passwort-Link nur als SHA-256 in email_verifications (Migration 11), 48 Stunden gültig; danach löscht die Verdichtung das unbestätigte Konto. Bei Freigabe-Modi erfahren Admins erst nach der Bestätigung davon |
ALLOW_REGISTRATION |
false |
true = REGISTRATION_MODE=open; false (Standard der Vorlagen) lässt die Wahl dem Admin-UI |
LOG_LEVEL / DEBUG |
info / false |
debug, info, warn, error; DEBUG=true erzwingt debug |
MINDA_DEMO (auch MINDSIFT_DEMO, DEMO_MODE) |
false |
Demo-Konten anlegen. Die Demo verschickt nichts (keine Alarme, Test- oder Hinweismails, Webhooks), Server-Einstellungen sind schreibgeschützt, Passwort, E-Mail und Konten der gemeinsam genutzten Demo-Konten lassen sich nicht ändern (notInDemo in internal/api/demo.go). Wer auf einem Gerät noch kein Theme gewählt hat, sieht die Demo in Fjord Nacht (rund) statt im eckigen Standard (DEMO_THEME in lib/theme.ts) |
DEMO_RESET_MINUTES |
20 |
Demo-Modus: periodischer Reset auf die Beispieldaten (0 = aus). Feste IDs der Demo-Konten (usr_demo_admin, usr_demo_user) halten Sitzungen gültig; laufende Anfragen sperren den Reset (internal/api/demo.go, Streams ausgenommen); danach DEMO_RESET an alle Verbindungen, /auth/status liefert demoNextReset/demoResetInSeconds. |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM, SMTP_TLS |
leer / 587 / true |
Mailversand, auch im Admin-UI einstellbar |
SMTP_FROM_NAME |
MindSift |
Anzeigename des Absenders, wenn SMTP_FROM nur eine Adresse ist (From: "MindSift" <adresse>, der SMTP-Umschlag bleibt die reine Adresse); ein Name in SMTP_FROM hat Vorrang |
SMTP_SKIP_VERIFY |
false |
Zertifikat des Mailservers nicht prüfen (nur interne Server mit selbstsigniertem Zertifikat) |
NOTIFY_DIGEST_MINUTES |
5 |
Benachrichtigungen (Freigaben, beobachtete Aufgaben …) werden gesammelt und höchstens alle so viele Minuten gebündelt verschickt |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY |
– | Schlüsselpaar für Web-Push; ohne Angabe einmal erzeugt und in push_keys gespeichert |
VAPID_SUBJECT |
BASE_URL (wenn https) |
Kontakt für die Push-Dienste (mailto: oder https://) |
MEDIA_MAX_IMAGE_MB, MEDIA_MAX_AUDIO_MB |
15 / 50 |
Größtes Bild bzw. größte Audiodatei in Notizen in MB |
MEDIA_QUOTA_MB |
0 |
Speicherplatz für Medien pro Benutzer in MB (0 = unbegrenzt) |
MEDIA_DEMO_MAX_MB |
3 |
Nur im Demo-Modus: maximale Dateigröße für Medien-Uploads in MB |
STT_URL |
leer | Eigene Spracherkennung: Basis-URL eines OpenAI-kompatiblen Whisper-Servers (z. B. http://whisper:8000, speaches). Gesetzt = Server-Modus, sonst Spracherkennung des Browsers; in der Demo immer Browser |
STT_MODEL, STT_LANGUAGE, STT_API_KEY |
deepdml/faster-whisper-large-v3-turbo-ct2 / de / leer |
Modell, Sprache (auto = erkennen), Schlüssel falls der Whisper-Server einen verlangt |
STT_DISABLED |
false |
true blendet die Spracheingabe überall aus |
STT_MAX_SECONDS, STT_MAX_UPLOAD_MB |
600 / 25 |
Längste Aufnahme (stoppt im Browser), größte Aufnahme |
STT_MAX_PARALLEL, STT_PER_USER_PER_HOUR, STT_TIMEOUT_SECONDS |
1 / 60 / 120 |
Gleichzeitige Erkennungen (weitere warten), Erkennungen pro Person und Stunde, Höchstdauer inkl. Warten |
LEGAL_IMPRINT_URL, LEGAL_PRIVACY_URL |
leer | Impressum/Datenschutz als externer Link (nur http(s)) |
LEGAL_IMPRINT_CONTENT, LEGAL_PRIVACY_CONTENT |
leer | Impressum/Datenschutz als Markdown, angezeigt in einem Lesefenster |
LEGAL_IMPRINT_CONTENT_FILE, LEGAL_PRIVACY_CONTENT_FILE |
leer | Pfad zu einer Datei im Container mit Markdown-Inhalt für Impressum bzw. Datenschutz. Ohne URL und Text bleibt der Link unsichtbar. |
LEGAL_CUSTOM_LINK_LABEL, LEGAL_CUSTOM_LINK_URL |
leer | Eigener Link im globalen Desktop-Footer (nur https://, sonst ignoriert) |
Hinweis: DATABASE_URL mit postgres… führt dazu, dass der Standardpfad in DATA_DIR genutzt wird; Postgres ist nicht implementiert.
Authentifizierung
- Login mit E-Mail, Benutzername (aus dem E-Mail-Präfix) oder
login. Passwörter: bcrypt (DefaultCost). - Login-Sperre: Fehlversuche zählen im Speicher in drei Töpfen: Konto + Adresse (3), Konto (20), Adresse (30), je innerhalb von 15 Minuten; ist einer voll, gibt es 15 Minuten lang
429mitRetry-After. Unbekannte Namen zählen wie Konten (kein Erraten, welche existieren). Erfolg leert die Kontotöpfe, ein Passwort-Reset (Mail oder Admin) hebt die Sperre auf. Die Client-Adresse kommt nur von vertrauenswürdigen Proxys (TRUSTED_PROXIES). Dieselben Töpfe begrenzen pro Adresse Registrierungen (20) und „Passwort vergessen“ (10) je 15 Minuten; hier zählt jede Anfrage. - Einladungscodes:
INV-plus 100 Zufallsbits in Base32. Die Registrierung beansprucht den Code, bevor das Konto entsteht (kein doppeltes Einlösen). - JWT und Sitzungen: HS256, Claims
userId,email,displayName,tokenVersion,jti. Jede Anmeldung legt eine Zeile insessionsan (Gerät, IP, letzte Nutzung);jtiist deren ID. Gültig ist ein Token nur, wenn die Sitzung aktiv ist (nicht beendet, höchstens 30 Tage unbenutzt), das Konto aktiv ist undtokenVersionmitusers.token_versionübereinstimmt. Ist das Token älter als 24 Stunden, schickt der Server im HeaderX-Session-Tokenein neues für dieselbe Sitzung; das Frontend übernimmt es (request()inlib/api.ts). Aktive Geräte bleiben so angemeldet, unbenutzte fallen nach 30 Tagen heraus (internal/api/sessions.go). Neue Passwörter brauchen mindestens 8 Zeichen (auth.ValidatePassword).token_versionwird bei Passwortänderung, Rollenwechsel, Deaktivierung und „Alle Sitzungen beenden“ erhöht. - Logout beendet die Sitzung des Geräts;
GET /auth/sessionslistet die Geräte,DELETE /auth/sessions/{id}meldet eines ab. Passwort und Login-E-Mail ändern sich nur mit dem aktuellen Passwort (currentPassword, falsche Eingaben zählen für die Login-Sperre); ein neues Passwort meldet alle anderen Geräte ab, dieses Gerät erhält perX-Session-Tokenein neues Token. Ist SMTP eingerichtet, geht eine Hinweismail an die bisherige Adresse. - API-Keys:
mindsift_sec_<hex>, gespeichert als SHA-256, nur das Präfix bleibt sichtbar. Der Key gilt als Bearer-Token oder inX-API-Key. Scopeswebhook:*liefern Daten an (POST /inbox,POST /webhooks/ingest), die Agenten-Scopestasks:read,tasks:write,lists:read,lists:writegelten nur am MCP-Endpunkt (POST /mcp,internal/api/mcp.go); jedes Werkzeug prüft seinen Scope,tools/listzeigt nur erlaubte. Alle anderen Endpunkte antworten für Keys mit403; Notizen sind mit Keys nie erreichbar. - Passwort vergessen: nur wenn SMTP und
BASE_URLgesetzt sind (nicht im Demo-Modus;passwordResetin/auth/status).POST /auth/forgot-passwordantwortet immer gleich und verschickt die Mail im Hintergrund, höchstens alle 2 Minuten pro Konto. Der LinkBASE_URL/?resetToken=…gilt eine Stunde und einmal; gespeichert wird nur der SHA-256-Hash (password_reset_tokens).POST /auth/reset-passwordsetzt das Passwort, entwertet alle offenen Links des Kontos und erhöhttoken_version(alle Sitzungen enden). Die App entfernt den Token sofort aus der Adresszeile. - Registrierung: nur mit Einladungscode oder wenn
allow_registrationaktiv ist. Das erste Konto entsteht über/auth/setupund wird Administrator.
Medien in Notizen
Bilder und Audio liegen in $DATA_DIR/media/<docId>/<id>.<ext>, beschrieben in der Tabelle media (Migration 5, internal/storage/media.go). Ein Block verweist über seinen Inhalt darauf, z. B. Typ image mit ; damit laufen Verweise ohne Änderung am Sync-Modell durch Synchronisation, Rückgängig und Konflikte. Hochladen (POST /documents/{id}/media) braucht Schreibrecht auf die Notiz, Lesen (GET /media/{id}) Leserecht auf die Notiz der Datei oder auf eine Notiz, deren Blöcke sie zeigen (kopierte Blöcke). Das Format wird am Inhalt erkannt (JPEG, PNG, GIF, WebP; WebM, Ogg, MP3, WAV, M4A), SVG und alles andere wird abgelehnt; Antworten tragen eine eigene CSP mit sandbox. Externe Bilder kopiert der Server einmal (POST /documents/{id}/media/fetch, nur https und öffentliche Adressen wie bei Webhooks, nicht in der Demo). Grenzen: MEDIA_MAX_IMAGE_MB, MEDIA_MAX_AUDIO_MB, MEDIA_QUOTA_MB, in der Demo MEDIA_DEMO_MAX_MB. Backup-ZIP (media/<id><ext>) und Markdown-Archiv (attachments/, Einbettung  relativ zur Notiz) enthalten die Dateien der eigenen Notizen; beim Import bekommen sie neue IDs. Der Obsidian-Import übernimmt Bilder und Audio, die eine Notiz allein in einem Punkt einbettet (![[datei.png]] oder ); andere Dateien erscheinen in der Vorschau als übersprungen. Die Verdichtung (alle 6 Stunden) löscht Dateien, die kein Block mehr erwähnt (auch keine gelöschten im Papierkorb oder Protokoll), frühestens einen Tag nach dem Hochladen. Der Demo-Reset leert media/.
Notizen umbauen
internal/storage/restructure.go: Ast auslagern liest den Ast und legt die neue Notiz an (Titel eindeutig, gleiche direkte und Gruppen-Freigaben, Besitzer der Ursprungsnotiz bekommt Schreibrecht). Den Ast in der Ursprungsnotiz ersetzt der Editor selbst durch [[Titel]] (Hashtags bleiben am Link) – als eigene Änderung über die Sync-Engine, damit sie nicht mit ungesendeten Eingaben kollidiert und Strg+Z sie zurücknimmt; dabei legt /extract/undo die neue Notiz in den Papierkorb. Zusammenführen hängt die Quelle als h2-Überschrift mit ihren Punkten als Unterpunkte an die Zielnotiz an (ebenfalls über Operationen, offene Editoren folgen live) und legt die Quelle in den Papierkorb; „Rückgängig“ löscht die Überschrift samt Unterpunkten und stellt die Quelle wieder her. Die Aktion läuft im Namen des Editor-Clients (clientId), damit dessen eigene Block-Sperren nicht stören. Bilder und Aufnahmen funktionieren weiter, weil der Zugriff über die Notizen läuft, die sie zeigen.
Benachrichtigungen
internal/api/notify.go, internal/storage/notifications.go (Migration 7). Jede Person wählt pro Ereignisart die Kanäle – eigene E-Mail (Alarm-Adresse, sonst Konto-Adresse) eigener Discord-Webhook, eigener Webhook (customWebhookUrl, JSON per POST) und Web-Push an die eigenen Geräte (internal/api/push.go, Migration 9, Tabelle push_subscriptions; Geräte, die der Push-Dienst nicht mehr kennt (404/410), werden entfernt) –, gespeichert als JSON in users.notification_settings (GET /auth/me, PATCH /auth/settings → notificationSettings). Ereignisse: due (Fälligkeits-Alarm, sofort über den Alarm-Ticker), shared (Notiz, Ordner, Aufgabenliste oder Liste geteilt; bei Gruppen alle Mitglieder), taskDone (Aufgabe einer geteilten Liste erledigt), watched (jede Änderung an einer beobachteten Aufgabe, task_watchers, POST/DELETE /tasks/{id}/watch). Alles außer due landet in notification_queue; ein Job im Minutentakt schickt je Person eine gebündelte Nachricht, sobald das älteste Ereignis NOTIFY_DIGEST_MINUTES (Standard 5) alt ist (E-Mail als Text + HTML, Discord als ein Embed mit höchstens 10 Zeilen, eigener Webhook als JSON {"event":"digest","heading","items":[{"kind","title","text"}],"link","timestamp"}; fällige Aufgaben gehen dort als {"event":"task_due",…} einzeln raus), nach 3 Fehlversuchen wird verworfen. Eigene Änderungen benachrichtigen nie den Handelnden, außer bei beobachteten Aufgaben mit includeOwnWatched. paused stoppt alles, auch Alarme. Tageszusammenfassung: zur gewählten Uhrzeit (Server-Zeitzone, innerhalb von 30 Minuten, einmal pro Tag) überfällige, heutige und morgige offene Aufgaben samt Anzahl der folgenden 7 Tage; nichts fällig → keine Nachricht. Im Demo-Modus laufen die Jobs nicht.
Link-Vorschau
Punkte mit einer reinen https-Adresse zeigen darüber eine Karte (LinkPreviewCard, lib/linkPreview.ts). GET /link-preview?url= holt die Seite serverseitig mit demselben Schutz wie Webhooks (nur https, keine internen Adressen, Weiterleitungen geprüft), liest og:*/twitter:*, <title>, description und das Icon aus dem <head> (höchstens 1 MB HTML, Bild 2 MB, Icon 100 KB, kein SVG) und speichert alles in link_previews (Migration 6): Erfolg 7 Tage, nichts Brauchbares 1 Tag, nach 30 Tagen gelöscht. Bilder kommen über GET /link-preview/{key}/image|icon mit Login. Pro Adresse höchstens 300 Abrufe in 15 Minuten (Antworten aus dem Cache zählen nicht). Ausblenden einer Karte macht aus der Adresse [Titel](url) (ein benannter Link hat keine Karte), damit synchronisiert die Wahl mit der Notiz; „Link-Vorschauen anzeigen“ unter Darstellung gilt pro Gerät. Im Demo-Modus aus.
Spracherkennung
Nur über Umgebungsvariablen (STT_*, siehe oben), nicht in der Oberfläche; Administration → Server & Isolation zeigt den aktiven Modus, den Zustand des Whisper-Servers und bietet eine Testaufnahme. Alle Mikrofon-Knöpfe (Quick-Dump-Leiste, mobiles Erfassen-Sheet, Halten des +-Knopfs) nutzen denselben Hook useVoiceInput (frontend/src/lib/voice.ts, useVoiceInput.ts): server nimmt auf und zeigt „Wird erkannt…“, browser liefert Text schon beim Sprechen, off oder ein Browser ohne die nötige Funktion blendet das Mikrofon aus. Im Server-Modus zeigen Wellen den echten Mikrofonpegel (VoiceWave), die Aufnahme endet spätestens nach STT_MAX_SECONDS. Der Browser nimmt per MediaRecorder auf (WebM/Opus, in Safari MP4/AAC) und schickt die Aufnahme an POST /voice/transcribe; MindSift reicht sie an STT_URL/v1/audio/transcriptions weiter (model, language) und gibt nur den Text zurück. Nichts wird gespeichert oder protokolliert außer Fehlern (internal/api/voice.go). Beim Start wartet MindSift auf den Whisper-Server und stößt den Download des Modells an, falls es fehlt (POST /v1/models/<id> bei speaches).
docker-compose.yml enthält den Dienst whisper (speaches 0.8.3, CPU) als auskommentierten Block: Rauten entfernen, STT_URL=http://whisper:8000 in .env setzen, docker compose up -d; das Modell liegt in ./whisper (Ordner gehört UID 1000). Gemessen auf einem Desktop mit 16 Kernen (int8) für einen Satz von 4 s: Systran/faster-whisper-small 1,6 s, deepdml/faster-whisper-large-v3-turbo-ct2 8 s bei besserer Erkennung (RAM ca. 1 bzw. 2 GB). Auf geteilten vCPUs ist mit dem Mehrfachen zu rechnen.
Hintergrundjob: Alarme
startAlarmTicker prüft jede Minute alle offenen, noch nicht gemeldeten Aufgaben mit Fälligkeit. Ist der Alarmzeitpunkt erreicht (Fälligkeit mit Uhrzeit bzw. 9:00 bei ganztägigen Aufgaben, minus Vorlaufzeit des Benutzers, Zeitzone TZ), sendet er über die Kanäle des Benutzers (email, discord, webhook) und setzt alarm_sent, sobald mindestens ein Kanal erfolgreich war. Nach 3 Durchläufen ohne Erfolg gibt er auf (alarm_failures). Details und Einschränkungen: modules/tasks.md.
Hintergrundjob: Verdichtung der Notiz-Synchronisation
startCompaction räumt beim Start und dann alle 6 Stunden auf: abgelaufene Leases und Sitzungen im Speicher, Log-Einträge älter als 30 Tage (private Notizen: 7 Tage), Grabsteine älter als 30 Tage und Idempotenz-Ergebnisse älter als 90 Tage. Details: database.md.
Build
Stage 1 node:22-alpine tsc, vitest, vite build → frontend/dist
Stage 2 golang:1.23 go vet, go test, go build → /app/mindsift (dist wird eingebettet)
Stage 3 alpine:3.21 nur Binary, ca-certificates, tzdata
Image-Tags (CI): Push auf dev → :dev; Push/Merge auf main → :latest; Git-Tag v* (z. B. v1.2.0) → :v1.2.0 und :stable; Vorabversionen (v1.2.0-rc.1) nur :v1.2.0-rc.1, :stable bleibt. Andere Branches bauen nicht.
Versionierung: Ab v0.6.0 nach Semantic Versioning: MAJOR.MINOR.PATCH, Tags dreiteilig mit v (v0.6.0, v1.0.0-rc.1). Solange die Hauptversion 0 ist, erhöhen inkompatible Änderungen (API, Datenformate, Konfiguration) die mittlere Stelle, Fehlerbehebungen die letzte. Jede Änderung, die Benutzer oder Betreiber betrifft, kommt in CHANGELOG.md unter „Unreleased“ (Format Keep a Changelog); beim Release wird der Abschnitt zur Version mit Datum, frontend/package.json bekommt die Versionsnummer.
Die angezeigte Version (Kopf der App) setzt der Build-Parameter APP_VERSION: Die CI übergibt bei Git-Tags den Tag-Namen (v0.6.0), sonst zeigt die App die Version aus frontend/package.json mit -dev (v0.6.0-dev). Umgesetzt in frontend/vite.config.ts und frontend/src/version.ts.
Die Tests laufen beim Image-Build (Frontend: npm run test:run, Backend: go test -v ./...); ein Fehler bricht den Build ab. Frontend-Tests liegen unter frontend/src/**/__tests__/ (Vitest + jsdom + Testing Library, Setup in frontend/src/test/setup.ts).
Hintergrundjob: Backups
backups.go / backup.go: Ist „Automatische Backups“ in den Admin-Einstellungen an, läuft einmal täglich zur eingestellten Uhrzeit (backup_time, Zeitzone TZ; nach einem Neustart wird ein verpasster Lauf am selben Tag nachgeholt):
backups/system/mindsift-db-JJJJ-MM-TT-HHMM.db.gz: Abzug der ganzen Datenbank perVACUUM INTO(stimmig, auch während geschrieben wird), gzip.backups/users/<id>/mindsift-backup-….zip: dasselbe ZIP wie „Backup herunterladen“ für jedes aktive Konto, mit den Bildern und Aufnahmen der eigenen Notizen untermedia/.- Achtung: Der Datenbank-Abzug enthält keine Mediendateien. Wer den Server sichert, sichert den ganzen Ordner
data/(Datenbank,media/,backups/). - Danach werden Dateien älter als
backup_retention_days(Standard 3) gelöscht. Dateien entstehen über.partund Umbenennen, halbe Backups tauchen nie in der Liste auf. Die API liefert nur Dateien mit genau diesem Namensmuster aus. - Im Demo-Modus läuft kein Zeitplan. Zurückspielen eines Datenbank-Abzugs: Container stoppen,
mindsift.dbdurch die entpackte Datei ersetzen, starten. Benutzer spielen ihre eigenen Backups in den Kontoeinstellungen ein, auch nur für einzelne Bereiche (Notizen, Aufgaben, Listen, Inbox, Themes).
Offline (PWA)
- Service Worker (public/sw.js): Seitenaufrufe Network-First mit der zuletzt geladenen
index.htmlals Rückfall, Bundles unter/assets/(Hash im Namen) aus dem Cache, Icons und Manifest Stale-While-Revalidate. API-Anfragen fasst er nie an. Alte Caches verschwinden beim Aktivieren einer neuenVERSION, Bundles sind auf 80 Einträge begrenzt. - Lese-Cache (lib/offline.ts): Jede erfolgreiche GET-Antwort landet pro Benutzer in IndexedDB (
mindsift-offline). Ohne Verbindung liefertrequest()die letzte bekannte Antwort; so startet die App offline angemeldet und zeigt Gedanken, Aufgaben und Listen. Ausgenommen: Streams, Long-Polling, Notiz-Sync (eigener Speicher, modules/collaboration.md), Export, Suche. Abmelden leert Cache und Warteschlange. - Warteschlange (Outbox): Offline erfasste Gedanken (
POST /inbox) und abgehakte Aufgaben bzw. Listeneinträge (PATCH) werden inlocalStoragegesammelt, wiederholtes Umschalten zählt nur mit dem Endstand. Gesendet wird in Reihenfolge beimonline-Ereignis, beim Start und alle 30 s, solange etwas wartet; vom Server abgelehnte Einträge (4xx) entfallen. Andere Änderungen brauchen weiterhin eine Verbindung. - Hinweis: Ein Banner über der unteren Navigation zeigt „Offline“ und die Zahl wartender Änderungen bzw. „Synchronisiere…“.
- Verbindungsstatus im Kopf (lib/useServerOnline.ts): Maßgeblich ist die app-weite Echtzeit-Verbindung (WebSocket/SSE), nicht
navigator.onLine; so fällt auch ein ausgefallener Server auf. Offline gilt erst nach 4 s ohne Verbindung (Start, kurze Reconnects), ohne Gerätenetz sofort. Desktop: „● offline“ unter dem Namen, nur ohne Verbindung; Handy: Punkt am Profilbild, grün oder rot.
App-Icons, PWA & Social-Preview (Open Graph)
MindSift verwendet plattformübergreifend ein einheitliches App-Icon-Design, das exakt dem Icon im Kopfbereich der mobilen App entspricht:
- Design & Geometrie: Sanft abgerundetes Quadrat (
rx=108auf 512x512 Canvas bzw.rounded-xl), Primärblau (--accent-primary: #2563eb) als Hintergrund und zentriertes Lucide Brain Icon in reinem Weiß (--text-on-accent: #ffffff).
Asset-Matrix (frontend/public/)
| Datei | Zweck & Zielplattform | Besonderheit |
|---|---|---|
icon.svg |
Modernes Standard-Icon & SVG-Favicon | Vektor-Original mit abgerundeten Ecken |
icon-192x192.png, icon-512x512.png |
PWA-Installation (purpose: "any"), Desktop Linux/Windows |
Erforderlich für Chromium unter Linux (erzeugt sonst kein .desktop-Icon) |
icon-maskable-192x192.png, icon-maskable-512x512.png, icon-maskable.svg |
Android Adaptive Icons (purpose: "maskable") |
Full-Bleed Hintergrund (#2563eb) ohne transparente Ecken, Brain im 80%-Sicherheitsbereich |
apple-touch-icon.png (180x180) |
iOS Safari („Zum Home-Bildschirm“) | Opaker Hintergrund verhindert schwarze Ränder beim automatischen Apple-Squircle-Maskieren |
favicon.ico, favicon-32x32.png, favicon-16x16.png |
Browser-Tabs, Lesezeichen, Chronik | Multi-Resolution-ICO (16/32/48) für alle Desktop-Browser |
og-image.png (1200x630), og-image-square.png (512x512) |
Link-Vorschau in WhatsApp, Discord, Telegram, X/Twitter | Dunkler #0b0f19-Hintergrund mit Icon & Claim; Datei-Größe < 120 KB (WhatsApp-Limit < 300 KB) |
Dynamische OpenGraph-URL-Injektion
Messenger-Crawler (insbesondere WhatsApp) werten relative Bild-URLs in og:image oft nicht korrekt aus. Der Go-SPA-Handler in internal/api/server.go ersetzt beim Ausliefern von index.html automatisch relative Pfade (/og-image.png, og:url) durch die absolute Server-Origin (scheme://host), ermittelt über X-Forwarded-Proto und X-Forwarded-Host bzw. req.Host.