No description
  • TypeScript 55%
  • Kotlin 34.3%
  • JavaScript 5.2%
  • QML 3.6%
  • Shell 0.8%
  • Other 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jonas Krug 5480c88429 Plasma-Widget 1.2.0: formatierte Vorschau, Zeilenumbrüche in der Vollansicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:50:17 +00:00
android WYSIWYG-Editor (Web + Android), Durchgestrichenes überall formatiert anzeigen 2026-10-17 01:43:21 +00:00
docker WYSIWYG-Editor (Web + Android), Durchgestrichenes überall formatiert anzeigen 2026-10-17 01:43:21 +00:00
e2e Transkription offline vormerken und Transkript in den Notiztext übernehmen 2026-10-13 10:52:14 +00:00
plasma Plasma-Widget 1.2.0: formatierte Vorschau, Zeilenumbrüche in der Vollansicht 2026-10-17 01:50:17 +00:00
scripts WYSIWYG-Editor (Web + Android), Durchgestrichenes überall formatiert anzeigen 2026-10-17 01:43:21 +00:00
server Transkription offline vormerken und Transkript in den Notiztext übernehmen 2026-10-13 10:52:14 +00:00
web WYSIWYG-Editor (Web + Android), Durchgestrichenes überall formatiert anzeigen 2026-10-17 01:43:21 +00:00
.dockerignore Merkzettel: selbst gehostete Notiz-App als GlassKeep-Nachfolger 2026-10-12 21:23:40 +00:00
.env.example Merkzettel: selbst gehostete Notiz-App als GlassKeep-Nachfolger 2026-10-12 21:23:40 +00:00
.gitignore WYSIWYG-Editor (Web + Android), Durchgestrichenes überall formatiert anzeigen 2026-10-17 01:43:21 +00:00
compose.test.yaml Merkzettel: selbst gehostete Notiz-App als GlassKeep-Nachfolger 2026-10-12 21:23:40 +00:00
compose.yaml Merkzettel: selbst gehostete Notiz-App als GlassKeep-Nachfolger 2026-10-12 21:23:40 +00:00
README.md WYSIWYG-Editor (Web + Android), Durchgestrichenes überall formatiert anzeigen 2026-10-17 01:43:21 +00:00
SPEC-NOTES.md Merkzettel: selbst gehostete Notiz-App als GlassKeep-Nachfolger 2026-10-12 21:23:40 +00:00
SPEC.md Bento-Raster und Notizen per Drag & Drop zu Ordnern zusammenfassen (Web + Android) 2026-10-13 17:36:34 +00:00

Merkzettel

Selbst gehostete Notiz-App, Nachfolger von GlassKeep: Web-App (PWA, offline-fähig), native Android-App mit lokaler Datenhaltung und Sync, Medien (Bilder, Audio/Sprachmemos, Video, Dateien, Zeichnungen), Erinnerungen, Versionsverlauf, Papierkorb, Freigabelinks, Volltext- und KI-Suche.

Der verbindliche Vertrag zwischen Server, Web und Android steht in SPEC.md, Abweichungen und Auslegungen in SPEC-NOTES.md.

Stack-Verzeichnis /opt/stacks/merkzettel (in Dockge sichtbar)
Container merkzettel (Image merkzettel-app:latest)
URL (LAN) http://192.168.56.136:8094
Daten ./data → /app/data (SQLite + Medien)
Android-App http://192.168.56.136:8094/download/merkzettel.apk
KDE-Plasma-Widget http://192.168.56.136:8094/download/merkzettel.plasmoid (Quellen in plasma/)

1. Betrieb

cd /opt/stacks/merkzettel
docker compose up -d --build      # bauen (inkl. Android-APK) und starten
docker compose ps                 # Status – "healthy" nach ca. 10–40 s
docker compose logs -f app        # Logs (JSON, pino)
docker compose down               # stoppen (Daten bleiben in ./data)

Der erste Build dauert lange (Android-SDK + Gradle-Abhängigkeiten, ~10–20 min). Danach greifen die BuildKit-Caches (merkzettel-gradle, merkzettel-npm); nur geänderte Teile werden neu gebaut. Ein Update ist einfach erneut docker compose up -d --build – die App-Version (versionCode) steigt dabei automatisch, installierte Android-Apps melden das Update selbst.

Konfiguration (.env)

.env liegt neben compose.yaml (Rechte 600, nicht weitergeben). Alle Variablen sind in .env.example dokumentiert. Die wichtigsten:

Variable Wert hier Bedeutung
JWT_SECRET zufällig (64 Zeichen) Signatur der Zugriffstoken. Ändern = alle müssen sich neu anmelden.
PUBLIC_URL http://192.168.56.136:8094 Basis für Freigabelinks, apk.json und die in der APK voreingestellte Server-URL. Nach Einrichtung des Reverse Proxys auf https://… umstellen und neu bauen.
ADMIN_EMAILS jonas Diese Benutzernamen werden beim Start Admin (wie in GlassKeep; ADMIN_USERNAMES ist gleichwertig).
TRUST_PROXY 192.168.56.37 Nur diesem Proxy (NPM) wird X-Forwarded-For/-Proto geglaubt (Rate-Limits, Logs). Liste von IPs/CIDRs möglich; true (= jedem) nur hinter einem Proxy, der den Header immer überschreibt. Ohne Proxy weglassen.
ALLOW_REGISTRATION false Nur Startwert – danach gilt der Schalter in der Admin-Oberfläche.
GLASSKEEP_IMPORT_DB /import/glasskeep-notes.db Auto-Import beim allerersten Start (siehe 2.1).
LLM_BASE_URL http://host.docker.internal:11434/v1 llama-swap auf dem Host. Leer = KI aus. Nicht erreichbar = KI-Funktionen blenden sich aus, alles andere läuft weiter.
LLM_CHAT_MODEL / LLM_EMBED_MODEL qwen2.5:7b / embeddinggemma Modelle für „Frag deine Notizen“ und die semantische Suche.
TRANSCRIBE_BASE_URL / TRANSCRIBE_MODEL leer / – OpenAI-kompatibler Whisper-Endpunkt (…/v1, POST /audio/transcriptions) für Sprachmemo-Transkription. Leer = Transkription aus und in allen Oberflächen ausgeblendet. Einrichtung siehe 1.1.

Nach einer Änderung an .env: docker compose up -d (bei PUBLIC_URL zusätzlich --build, weil die URL in die APK eingebaut wird).

1.1 Sprachmemo-Transkription (optional, standardmäßig aus)

Merkzettel braucht dafür einen Endpunkt mit der OpenAI-API POST /v1/audio/transcriptions. Der vorhandene Home-Assistant-Whisper (ha-whisper-gpu, Port 10300) spricht das Wyoming-Protokoll und passt nicht. Der Stack bringt deshalb einen optionalen Dienst whisper mit (speaches, faster-whisper auf der CPU, int8, nur im Stack-Netz erreichbar, kein Host-Port). Einschalten – drei Zeilen in .env:

COMPOSE_PROFILES=transkription
TRANSCRIBE_BASE_URL=http://whisper:8000/v1
TRANSCRIBE_MODEL=Systran/faster-whisper-small

danach docker compose up -d. Beim ersten Start lädt merkzettel-whisper das Modell (~480 MB) in das Volume merkzettel-whisper-models; spätere Starts laden nichts mehr. TRANSCRIBE_MODEL muss gesetzt sein und gilt für App und Whisper-Dienst (der App-Standard whisper-1 wäre bei speaches ein Alias auf das große large-v3, das nicht vorab geladen wird). Genauer, aber langsamer: Systran/faster-whisper-medium. Ressourcen: bis 3 GB RAM während einer Transkription, 4 CPU-Threads (WHISPER_CPU_THREADS), das Modell wird nach 5 min Leerlauf entladen. Ein 10-Sekunden-Memo dauert mit small wenige Sekunden.

Danach zeigen Web- und Android-App bei Audio-Anhängen „Transkribieren“, in den Einstellungen gibt es „Sprachmemos automatisch transkribieren“; Transkripte sind durchsuchbar. Beides nur für Nutzer, die unter Einstellungen → KI „KI-Assistent aktivieren“ eingeschaltet haben (sonst 403 ai_disabled). Prüfen: curl -s http://192.168.56.136:8094/api/config → "transcription":true, und e2e/run-sync.sh führt dann zusätzlich den Transkriptionstest aus. Ausschalten: die drei Zeilen entfernen, docker compose up -d --remove-orphans.

1.2 Sicherheit des Containers

  • Der App-Container läuft als uid 1000 (orion) ohne Capabilities (cap_drop: ALL), mit no-new-privileges, schreibgeschütztem Root-Dateisystem (read_only) und Limits (3 GB RAM, 512 Prozesse). Beschreibbar sind nur ./data und ein 256-MB-/tmp (tmpfs) – wichtig, weil der Server fremde Medien mit ffmpeg, pdftoppm, sharp und ZIP-Entpacker verarbeitet.
  • ./data enthält Passwort-Hashes, Token-Hashes und alle privaten Notizen. Der Container setzt beim Start data/ auf 0700, Datenbank und Import-Backups auf 0600 und arbeitet mit umask 077 – andere lokale Konten (z. B. kiosk) können nichts davon lesen. Root (Backups, Dockge) ist davon nicht betroffen.

Kommandozeile im Container

docker exec -it merkzettel node dist/cli.js list-users
printf '%s' 'neues-passwort' | docker exec -i merkzettel node dist/cli.js reset-password jonas
printf '%s' 'passwort' | docker exec -i merkzettel node dist/cli.js create-admin anna --name "Anna" --admin
docker exec -it merkzettel node dist/cli.js delete-user anna
docker exec -it merkzettel node dist/cli.js purge-trash | gc-media | reindex-fts

Wurde beim ersten Start kein Admin importiert, legt der Server admin an; das Passwort steht dann in data/initial-admin-password.txt (wird nach der ersten Passwortänderung gelöscht).


2. Import aus GlassKeep

Importiert werden Benutzer (mit ihren alten Passwörtern und geheimen Schlüsseln), alle Notizen (Text, Checklisten, Zeichnungen → Bild-Anhang), Tags, Farben, Anheften/Archiv, eingebettete Bilder (als Mediendateien) und Mitbearbeitungen. Notiz-IDs bleiben erhalten. Vor jedem Import wird data/backups/pre-import-<zeit>.db angelegt (die neuesten 5 bleiben). Der Import arbeitet immer auf einer Kopie – die GlassKeep-Datenbank selbst wird nie geöffnet oder verändert.

2.1 Automatisch beim ersten Start (eingerichtet)

compose.yaml mountet ./reference nur lesend nach /import; .env setzt GLASSKEEP_IMPORT_DB=/import/glasskeep-notes.db. Beim allerersten Start (leere Benutzertabelle) wird diese Datei im Modus merge importiert. Danach passiert beim Neustart nichts mehr (glasskeepAutoImportDone ist gesetzt).

2.2 Final-Import mit frischem Snapshot (Umstieg)

Solange GlassKeep parallel weiterläuft, können dort noch Änderungen entstehen. Zum endgültigen Umstieg:

# 1. Konsistenten Snapshot der laufenden GlassKeep-DB ziehen (liest nur, GlassKeep darf weiterlaufen)
#    (die GlassKeep-Dateien gehören root, daher sudo; der Snapshot gehört danach orion)
sudo sqlite3 /opt/stacks/glasskeep/glass-keep/notes.db \
  ".backup /opt/stacks/merkzettel/reference/glasskeep-notes.db"
sudo chown orion:orion /opt/stacks/merkzettel/reference/glasskeep-notes.db
chmod 600 /opt/stacks/merkzettel/reference/glasskeep-notes.db

# 2. Probelauf: zeigt, was neu/aktualisiert würde, ändert nichts
docker exec merkzettel node dist/cli.js import-glasskeep /import/glasskeep-notes.db --mode update --dry-run

# 3. Echt importieren
docker exec merkzettel node dist/cli.js import-glasskeep /import/glasskeep-notes.db --mode update
  • --mode update übernimmt in GlassKeep geänderte Notizen, fügt neue Notizen/Benutzer/ Mitbearbeitungen hinzu und übernimmt in GlassKeep geänderte Passwörter/Schlüssel/Admin-Rechte sowie Anheften/Archiv/Reihenfolge. Vor dem Überschreiben wird ein Versionsstand („Import“) gespeichert – im Versionsverlauf der Notiz wiederherstellbar.
  • Wurde etwas in beiden Apps geändert, gewinnt Merkzettel: bei Notizen bleibt die Merkzettel-Fassung, die GlassKeep-Fassung kommt als Kopie „… (Import-Konflikt)“ dazu; bei Konten bleibt der Merkzettel-Stand. Beides steht als Warnung im Bericht.
  • --mode merge (Standard) fügt nur Neues hinzu und lässt Bestehendes unverändert.
  • In Merkzettel gelöschte Notizen und Benutzer werden nie wiederbelebt.
  • Ein zweiter Lauf mit derselben Datei ändert nichts (idempotent).
  • Danach GlassKeep stoppen (in Dockge) – am besten den Snapshot erst ziehen, wenn niemand mehr in GlassKeep schreibt.

2.3 Weitere Wege

  • Admin-Oberfläche → Admin → „GlassKeep-Datenbank importieren“ (Datei-Upload, Modus wählbar, Probelauf).
  • Pro Benutzer in den Einstellungen → Daten: GlassKeep-JSON-Export, Google-Keep-Takeout (ZIP/JSON), Markdown-Dateien und das verlustfreie Merkzettel-ZIP. Diese importieren immer ins eigene Konto.

3. Backup

Zu sichern sind zwei Dinge:

  1. data/ – Datenbank (merkzettel.db + -wal/-shm), Mediendateien (media/, derived/), secrets.json (Schlüssel für signierte Medienlinks, Web-Push). Konsistent sichern:
    sqlite3 /opt/stacks/merkzettel/data/merkzettel.db ".backup /pfad/zum/backup/merkzettel.db"
    rsync -a --exclude 'merkzettel.db*' --exclude tmp/ /opt/stacks/merkzettel/data/ /pfad/zum/backup/data/
    
    (Alternativ Container kurz stoppen und data/ komplett kopieren.) derived/ lässt sich zur Not neu erzeugen, media/ nicht.
  2. android/keystore/ – merkzettel-release.jks und keystore.properties. Ohne Keystore keine Updates der App möglich: eine mit einem anderen Schlüssel gebaute APK lässt sich nicht über die installierte App installieren; dann bleibt nur Deinstallieren (nicht synchronisierte lokale Änderungen gehen verloren). Den Keystore einmalig an einem sicheren Ort ablegen (Passwortmanager/Offline-Backup). scripts/gen-keystore.sh überschreibt ihn nie.

Vor jeder Datenbank-Migration legt der Server selbst eine Sicherung data/backups/pre-migration-<NNN>-<Zeit>.db an (die neuesten 5 bleiben); ohne erfolgreiche Sicherung startet er nicht.

.env (enthält JWT_SECRET) ebenfalls sichern; geht sie verloren, reicht ein neues Secret – dann müssen sich nur alle neu anmelden.


4. Android-App

  1. Auf dem Handy http://192.168.56.136:8094/download/merkzettel.apk öffnen (oder in der Web-App: Einstellungen → App → QR-Code scannen).
  2. Installation aus dieser Quelle erlauben, APK installieren.
  3. Server-URL ist vorbelegt (PUBLIC_URL zum Build-Zeitpunkt), mit dem bisherigen GlassKeep-Login anmelden.

Die App hält alle Notizen lokal (Room-Datenbank), funktioniert komplett offline und synchronisiert Änderungen, sobald wieder eine Verbindung besteht (WorkManager, alle 15 min und sofort nach Änderungen, live per Server-Sent Events, solange die App offen ist). Konflikte gleichzeitiger Bearbeitung werden feldweise zusammengeführt; bei echtem Textkonflikt entsteht eine Kopie „… (Konflikt )“.

Updates: Nach jedem docker compose up -d --build meldet die App „Update verfügbar“ (prüft /download/apk.json, SHA-256 wird verifiziert).

Hinweis: Die Android-App funktioniert über reines http:// im LAN vollständig (auch Sprachmemos aufnehmen – das macht die App nativ). Für die Web-App im Browser gelten über http:// die Einschränkungen aus Abschnitt 5. Für den Zugriff von unterwegs den Reverse Proxy (Abschnitt 5) einrichten und in der App die https://-Adresse eintragen.

4.1 Ordner (Web und App)

  • Notizen lassen sich in Ordner einsortieren, bis zu 3 Ebenen tief (Ordner › Unterordner › Unterordner), höchstens 500 Ordner pro Benutzer. Jede Notiz liegt in höchstens einem Ordner; ohne Ordner erscheint sie unter „Ohne Ordner“. „Notizen“ zeigt weiterhin alle aktiven Notizen, egal in welchem Ordner.
  • Ordner sind privat: Bei geteilten Notizen sortiert jeder Mitbearbeiter in seine eigenen Ordner ein und sieht die Ordner der anderen nicht.
  • Seitenleiste (Web) bzw. Menü (App), Abschnitt „Ordner“: Baum zum Auf-/Zuklappen mit Anzahl, „Ohne Ordner“, „Neuer Ordner“. Über ⋮ / Rechtsklick (Web) bzw. langes Drücken (App): Unterordner erstellen, Umbenennen, Farbe, Symbol (Emoji), Verschieben…, Löschen.
  • Löschen eines Ordners löscht nie Notizen: Notizen (auch archivierte und im Papierkorb) und Unterordner wandern in den übergeordneten Ordner bzw. nach „Ohne Ordner“.
  • Notiz einsortieren: Ordner-Chip im Editor bzw. auf der Karte, Mehrfachauswahl → „Verschieben nach…“ (mit Rückgängig), im Web auch per Drag & Drop einer Karte auf einen Ordner in der Seitenleiste (nur wenn die Seitenleiste in den Einstellungen auf „immer anzeigen“ steht).
  • In der Ordneransicht angelegte Notizen landen direkt in diesem Ordner; der Schalter „Unterordner einbeziehen“ zeigt auch die Notizen der Unterordner. Die Suche hat einen Filter „Ordner“ (inkl. Unterordner).
  • ZIP-Export legt die Notizen in Ordnerpfaden ab (markdown/<Ordner>/<Unterordner>/…); der Archiv-Import stellt die Ordner wieder her.

4.2 Teilen (z. B. per WhatsApp)

In der Notiz ⋮ → „Teilen…“ oder das Teilen-Symbol oben (Web und App); in der Mehrfachauswahl „Teilen“ (eine Notiz → Auswahl wie unten, mehrere → alle zusammen als Text). Die empfohlene Variante steht oben:

Option Ergebnis
Als Text Titel + Inhalt als normale Nachricht; Markdown wird zu lesbarem Text, Checklisten werden zu ☐/☑-Zeilen, fertige Transkripte von Sprachmemos werden angehängt
Als Bild die Notiz als PNG (helles Design in der Notizfarbe, bis zu 4 Bilder, Schlagwörter, optional Fußzeile „Merkzettel · Datum“)
Anhänge (n) Sprachmemos, Bilder, Videos, Dateien im Original – empfohlen, wenn die Notiz nur aus Anhängen besteht (z. B. ein reines Sprachmemo)
In Zwischenablage kopieren der Text aus „Als Text“

Alles wird lokal auf dem Gerät erzeugt, es wird kein externer Dienst angesprochen. In der App öffnet sich das normale Android-Teilen-Menü (WhatsApp, Signal, Mail …). Im Browser wird das System-Teilen genutzt, wo es das gibt (Handy, über HTTPS); sonst wird der Text kopiert bzw. das Bild/die Datei heruntergeladen. „Link teilen…“ (öffentlicher Freigabelink) gibt es weiterhin separat.

4.3 Startbildschirm-Widget (App)

Jedes Widget wird beim Hinzufügen einzeln eingerichtet (und lässt sich später über langes Drücken → Einstellungen ändern):

  • Quelle: angeheftete Notizen, alle Notizen, ein Schlagwort oder ein Ordner (optional mit Unterordnern).
  • Anzeige: nur Titel, Vorschau oder ganzer Inhalt; Sortierung; Anzahl der Notizen.
  • Schnellaktionen (neue Text-/Checklisten-/Sprachnotiz – bei einem Ordner-Widget direkt in diesem Ordner), Abhaken von Checklisten-Punkten direkt im Widget, transparenter Hintergrund.
  • Wurde der Ordner gelöscht, zeigt das Widget „Ordner nicht gefunden – Widget neu einrichten“.

4.4 KDE-Plasma-Widget (Desktop/Panel, Plasma 6)

Merkzettel als Plasmoid li.krg.merkzettel für den KDE-Plasma-6-Desktop oder das Panel: angeheftete Notizen, alle Notizen, ein Tag oder ein Ordner (optional mit Unterordnern, oder „Ohne Ordner“) im Blick behalten, Listen abhaken, schnell neue Notizen/Listen erfassen – auch offline. Einzelheiten in plasma/README.md.

  1. Herunterladen: in der Web-App Einstellungen → KDE-Plasma-Widget → „Plasma-Widget herunterladen“ (dort steht auch die Version) oder direkt http://192.168.56.136:8094/download/merkzettel.plasmoid.
  2. Installieren bzw. aktualisieren (im Ordner mit der Datei):
    kpackagetool6 -t Plasma/Applet -i merkzettel.plasmoid   # erstmalig
    kpackagetool6 -t Plasma/Applet -u merkzettel.plasmoid   # Update
    systemctl --user restart plasma-plasmashell             # nach einem Update, damit Plasma die neuen Dateien lädt
    
  3. Rechtsklick auf Desktop oder Panel → Widgets hinzufügen … → „Merkzettel“ hinziehen, dann im Widget anmelden (Server http://192.168.56.136:8094 bzw. später https://…).

Das Widget erscheint unter Einstellungen → Sitzungen als „Plasma-Widget “ mit der Kennzeichnung „KDE-Plasma-Widget“ (Sitzungsart plasma; an Servern vor dieser Version meldet es sich automatisch als android-Sitzung an). Das Paket wird beim Image-Bau aus plasma/package gebaut (Stage plasmoid im Dockerfile, plasma/build.sh) – es ist also immer auf dem Stand des Repos; GET /api/config meldet features.plasmoid, die Versionsdaten liegen unter /download/merkzettel-plasmoid.json.


5. Reverse Proxy (Nginx Proxy Manager auf 192.168.56.37)

Der Proxy ist nicht Teil dieses Stacks. Vorschlag für einen Proxy Host, z. B. notes.krg.li:

  • Forward: http://192.168.56.136:8094, „Websockets Support“ an, „Block Common Exploits“ an, SSL: Let's-Encrypt-Zertifikat (Wildcard vorhanden), „Force SSL“ + HTTP/2 an.
  • Advanced → Custom Nginx Configuration:
# große Uploads (Videos, Audio): Merkzettel lädt in 4-MiB-Blöcken, einzelne Requests bis 32 MiB
client_max_body_size 64m;

# Server-Sent Events (/api/events): nicht puffern, lange offen halten
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
proxy_http_version 1.1;
proxy_set_header Connection "";

# Uploads direkt durchreichen statt zwischenzuspeichern
proxy_request_buffering off;

proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

(„Websockets Support“ setzt in NPM Upgrade/Connection-Header; Merkzettel nutzt SSE statt WebSockets – die Zeile proxy_set_header Connection ""; sorgt für dauerhafte Upstream-Verbindungen. Der Server sendet selbst X-Accel-Buffering: no auf dem Event-Stream.)

Danach in .env PUBLIC_URL=https://notes.krg.li setzen und docker compose up -d --build. Browser erlauben einige Funktionen nur in einem „sicheren Kontext“ (HTTPS oder localhost). Über http://192.168.56.136:8094 gilt für die Web-App deshalb:

Funktion über http:// im LAN
Anmelden, Notizen, Sync, Medien hochladen/ansehen, Zeichnen, Suche, KI funktioniert
Sprachmemo aufnehmen (Mikrofon) nicht möglich – der Knopf öffnet den Hinweis „Audioaufnahme ist nur über HTTPS möglich“; stattdessen eine Audiodatei anhängen oder in der Android-App aufnehmen
Offline-Start der Web-App (Service Worker), Installation als PWA, „Teilen-Ziel“ ausgeblendet
Erinnerungen als Browser-Benachrichtigung / Web-Push ausgeblendet; Erinnerungen erscheinen nur in der geöffneten App bzw. auf Android
In die Zwischenablage kopieren (Codeblöcke) funktioniert über eine Ersatzmethode

Mit HTTPS über den Reverse Proxy ist alles verfügbar. (Die Playwright-Suite startet Chromium mit --unsafely-treat-insecure-origin-as-secure, damit sie diese Funktionen auch über http:// testen kann – das ist kein Hinweis darauf, dass normale Browser das erlauben.)


6. Tests

Alle Tests laufen in Wegwerf-Containern (merkzettel-*, --rm); auf dem Host ist weder Node noch Java nötig.

cd /opt/stacks/merkzettel
scripts/test-all.sh            # Server-, Web-, Android-Unit-Tests + Web-E2E (compose.test.yaml), nacheinander

# gegen den LAUFENDEN Stack: jeder Lauf legt einen temporären Admin + Benutzer mit eigener Lauf-Kennung an
# ("e2e-<lauf-id>-…") und löscht danach genau diese – mehrere Läufe gleichzeitig stören sich nicht:
e2e/run-e2e.sh                 # Playwright: alle Web-Szenarien + Medien/Video/PDF + GlassKeep-JSON-Roundtrip
e2e/run-sync.sh                # Sync-Protokoll-Harness (Mehrgeräte-Szenarien, Konflikte, Dedup, Entzug)
                               #   + Sprachmemo-Transkription, falls eingerichtet (sonst übersprungen)
e2e/run-android-live.sh        # Android-SyncEngine (Robolectric) gegen den laufenden Server

Gegen einen anderen Container/Port: APP_CONTAINER=<name> BASE_URL=http://127.0.0.1:<port> e2e/run-sync.sh. Die Hilfsskripte selbst testet e2e/tests/common.test.sh (ohne Stack).

Einzelbefehle (SPEC §12):

# Server
docker build -t merkzettel-server-test -f docker/server-test.Dockerfile docker/
docker run --rm --name merkzettel-server-test -v $PWD/server:/work/server -v $PWD/reference:/work/reference:ro \
  -v merkzettel-npm-cache:/root/.npm -w /work/server merkzettel-server-test bash -lc "npm ci && npm run typecheck && npm test"
# Web
docker run --rm --name merkzettel-web-test -v $PWD/web:/work/web -v merkzettel-npm-cache:/root/.npm \
  -w /work/web node:22-bookworm bash -lc "npm ci && npm run typecheck && npm test && npm run build"
# Android (immer nur ein Gradle-Lauf gleichzeitig). Der WYSIWYG-Editor der App (WebView) ist der
# TipTap-Editor aus web/src/android-editor; ohne vorher gebautes Bundle fällt die App aufs Textfeld zurück.
scripts/build-android-editor.sh   # → android/app/src/main/assets/editor/ (im Image: Dockerfile-Stage "editor")
docker build -t merkzettel-android-builder -f docker/android-builder.Dockerfile docker/
docker run --rm --name merkzettel-android-test -v $PWD/android:/android -v merkzettel-gradle-cache:/root/.gradle \
  -w /android merkzettel-android-builder gradle --no-daemon testDebugUnitTest assembleDebug
# Gerätetest im Emulator (KVM, headless; adb im Container, Screenshots per `adb exec-out screencap -p`)
docker build -t merkzettel-android-emulator -f docker/android-emulator.Dockerfile docker/
docker run -d --name merkzettel-emu --device /dev/kvm --network host merkzettel-android-emulator \
  bash -c "emulator -avd mz -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -no-snapshot -port 5584 & sleep infinity"
# Web-E2E auf Wegwerf-Stack
docker compose -f compose.test.yaml up --build --abort-on-container-exit --exit-code-from e2e
docker compose -f compose.test.yaml down --rmi local
# Plasma-Widget: node-Tests + qmllint + QV4-Smoke (Image merkzettel-plasma-dev), danach Paket bauen
plasma/test/run-all.sh && plasma/build.sh

Die echte GlassKeep-Datenbank (reference/) wird nur im Server-Test import-reference (Zählwerte, keine Inhalte) und für den Auto-Import verwendet – nie in E2E-Läufen mit Screenshots/Traces.


7. Stand (2026-10-12) und offene Punkte

Produktiv eingespielt sind Ordner/Teilen mit Migration 003 (docker compose up -d --build, 2026-10-12 ~19:36). Die anschließenden Review-Fixes (Web: Outbox-Reihenfolge bei neuen Ordnern, Anhänge-Teilen mit Download-Fallback, ZIP-Pfade; Android: „Als Bild“ als mehrere Seiten, Einrückung im Bild, Text-Kürzung, Widget-Sperre) sind getestet, aber noch nicht eingespielt – dafür docker compose up -d --build ausführen und die neue APK installieren.

Testläufe nach den Review-Fixes (Wegwerf-Container):

Suite Ergebnis
Server (tsc + vitest) 216 Tests bestanden, 1 übersprungen (opt-in Test gegen DB-Kopie)
Web (typecheck, eslint, vitest, Build) 205 Tests bestanden
Android-Unit-Tests (Robolectric) + assembleDebug 169 Tests, 0 Fehler (3 Live-Tests ohne Server übersprungen)
Web-E2E auf Wegwerf-Stack (compose.test.yaml) 20 bestanden (inkl. Ordner und Teilen)
Live gegen Produktiv-Stack (Server-Stand mit Migration 003, alte Web-Oberfläche): run-sync.sh / run-android-live.sh / run-e2e.sh 14 bestanden + 1 übersprungen (Transkription aus) / 3 bestanden / 21 bestanden; e2e-Benutzer wieder gelöscht, echte Daten unverändert (2 Benutzer, 689 Notizen)

Sicherungen: data/backups/pre-folders-20261012-192628.db (manuell), data/backups/pre-migration-003-20261012-193603.db (automatisch) und data/backups/pre-folders-final-20261012.db (vor dem Einspielen der Review-Fixes).

Bekannte Einschränkungen beim Teilen/Ordnern:

  • Web „Anhänge“ teilt höchstens 10 Dateien / 50 MB und nur vom Browser erlaubte Typen (z. B. kein HEIC/ZIP) – sonst wird heruntergeladen. „Als Bild“ ist im Web ein einzelnes langes PNG, in der App mehrere Seiten à 1920 px.
  • Android-Outbox: Notiz in einen gerade offline neu angelegten Ordner verschieben, während schon eine ältere Änderung derselben Notiz wartet, kann die Ordnerzuordnung verlieren (im Web behoben, App folgt).

Offen bzw. bewusst so:

  • Umstieg: frischen GlassKeep-Snapshot importieren (2.2), NPM-Proxy anlegen (5), danach PUBLIC_URL=https://… + docker compose up -d --build, GlassKeep stoppen.
  • Web über http://: kein Mikrofon, kein Offline-Start/PWA, keine Browser-Benachrichtigungen (Tabelle in 5) – mit HTTPS über den Proxy verfügbar. Web-Push ist erst mit HTTPS sinnvoll.
  • Transkription ist eingebaut, aber aus (1.1) – Einschalten kostet bis 3 GB RAM, der Host ist knapp.
  • Android-Sprachmemos nehmen nur im Vordergrund auf (kein Foreground-Service/keine Zusatzrechte); beim Sperren des Bildschirms wird die Aufnahme beendet und das Bisherige gespeichert.