docs: renewed README and light/dark diagonal screenshots; wider hashtag tiles
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m13s
- README: current features (journal, focus mode, push, themes, sharing into the inbox), outgoing connections, :latest instead of :stable, VAPID_* and LEGAL_CUSTOM_LINK_*, new journal and phone screenshots - e2e/screenshots.sh: light and dark in the same state joined along a diagonal, own container/port per run - hashtag tiles at least 18rem wide chore(release): v0.15.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -6,6 +6,12 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.15.0] - 2026-10-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Hashtag-Übersicht: Kacheln sind breiter (so viele Spalten wie mit mindestens 18rem passen), Tag-Namen und Notiztitel haben mehr Platz.
|
||||||
|
- README überarbeitet und Screenshots erneuert: jedes Bild zeigt links das helle und rechts das dunkle Theme (diagonal geteilt), neu sind Journal und Notiz am Handy. `e2e/screenshots.sh` erzeugt die Paare selbst (`SHOT_LIGHT`/`SHOT_DARK` für andere Farbfamilien) und läuft wie die Tests mit eigenem Container und Port.
|
||||||
|
|
||||||
## [0.14.3] - 2026-10-31
|
## [0.14.3] - 2026-10-31
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -352,7 +358,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
|
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
|
||||||
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
|
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
|
||||||
|
|
||||||
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...dev
|
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.0...dev
|
||||||
|
[0.15.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...v0.15.0
|
||||||
[0.14.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.2...v0.14.3
|
[0.14.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.2...v0.14.3
|
||||||
[0.14.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.1...v0.14.2
|
[0.14.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.1...v0.14.2
|
||||||
[0.14.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.0...v0.14.1
|
[0.14.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.0...v0.14.1
|
||||||
|
|||||||
@@ -6,14 +6,16 @@
|
|||||||
[](#)
|
[](#)
|
||||||
[](#-schnellstart)
|
[](#-schnellstart)
|
||||||
[](#-deine-daten-gehören-dir)
|
[](#-deine-daten-gehören-dir)
|
||||||
[](#-schnellstart)
|
[](CHANGELOG.md)
|
||||||
|
|
||||||
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
|
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="screenshots/Triage_desktop.png" alt="MindSift Triage Desktop" width="85%" />
|
<img src="screenshots/Triage_desktop.png" alt="MindSift Triage am Desktop, links hell, rechts dunkel" width="85%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<sub>Alle Screenshots zeigen links das helle und rechts das dunkle Theme.</sub>
|
||||||
|
|
||||||
[Philosophie](#-die-philosophie) • [Funktionen](#-funktionen) • [Schnellstart](#-schnellstart) • [Betrieb](#-betrieb--konfiguration) • [Entwicklung](#-entwicklung) • [Dokumentation](#-dokumentation)
|
[Philosophie](#-die-philosophie) • [Funktionen](#-funktionen) • [Schnellstart](#-schnellstart) • [Betrieb](#-betrieb--konfiguration) • [Entwicklung](#-entwicklung) • [Dokumentation](#-dokumentation)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -36,11 +38,14 @@ Die meisten Notiz- und Aufgaben-Apps verlangen Struktur im falschen Moment: Wenn
|
|||||||
|
|
||||||
* Eingabeleiste im Kopf der App, jederzeit mit `C` oder `/` fokussierbar; auch per Spracheingabe: über den Browser oder datenschutzfreundlich über einen eigenen Whisper-Container (optional in `docker-compose.yml`, siehe [Architektur](Doku/architecture.md#spracherkennung)); auf Smartphones auch per Gedrückthalten des `+`-Knopfs (Hold-to-Record mit Pegel-Wellen).
|
* Eingabeleiste im Kopf der App, jederzeit mit `C` oder `/` fokussierbar; auch per Spracheingabe: über den Browser oder datenschutzfreundlich über einen eigenen Whisper-Container (optional in `docker-compose.yml`, siehe [Architektur](Doku/architecture.md#spracherkennung)); auf Smartphones auch per Gedrückthalten des `+`-Knopfs (Hold-to-Record mit Pegel-Wellen).
|
||||||
* Ziel beim Erfassen wählbar: Posteingang, Aufgabe, Listeneintrag oder Notiz.
|
* Ziel beim Erfassen wählbar: Posteingang, Aufgabe, Listeneintrag oder Notiz.
|
||||||
* **Triage-Deck:** `H`/`←` verwerfen, `L`/`→` zur Aufgabe, `T` Aufgabe mit Termin, `D` in Notiz verwandeln, Leertaste = später. Auf dem Smartphone per Wischgeste.
|
* **Triage-Deck:** `H`/`←` verwerfen, `L`/`→` zur Aufgabe, `T` Aufgabe mit Termin, `D` in Notiz verwandeln, Leertaste = später. Auf dem Smartphone per Wischgeste oder über große Knöpfe unter der Karte; die Warteschlange klappt bei Bedarf auf.
|
||||||
|
* **Teilen aus anderen Apps:** Als installierte App erscheint MindSift im Teilen-Menü des Smartphones; geteilte Texte und Links landen im Posteingang.
|
||||||
* Papierkorb mit Wiederherstellen.
|
* Papierkorb mit Wiederherstellen.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="screenshots/Mobile_Triage.png" alt="MindSift Mobile Triage" width="280px" />
|
<img src="screenshots/Mobile_Triage.png" alt="MindSift Triage am Smartphone" width="280px" />
|
||||||
|
|
||||||
|
<img src="screenshots/Mobile_Workspace.png" alt="MindSift Notiz am Smartphone" width="280px" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
### ✅ Aufgaben & Kanban
|
### ✅ Aufgaben & Kanban
|
||||||
@@ -53,7 +58,7 @@ Die meisten Notiz- und Aufgaben-Apps verlangen Struktur im falschen Moment: Wenn
|
|||||||
* **Wiederholungen:** Wiederkehrende Aufgaben (täglich, wöchentlich, monatlich, jährlich) mit automatischer Folgeaufgabe beim Abhaken.
|
* **Wiederholungen:** Wiederkehrende Aufgaben (täglich, wöchentlich, monatlich, jährlich) mit automatischer Folgeaufgabe beim Abhaken.
|
||||||
* **Tageszusammenfassung:** Tägliche Übersicht fälliger Aufgaben zur selbst gewählten Uhrzeit per E-Mail, Discord, eigenem Webhook oder Push.
|
* **Tageszusammenfassung:** Tägliche Übersicht fälliger Aufgaben zur selbst gewählten Uhrzeit per E-Mail, Discord, eigenem Webhook oder Push.
|
||||||
* **Push-Benachrichtigungen** auf Handy und Desktop (auch bei geschlossener App; iPhone/iPad als Home-Bildschirm-App), als eigener Kanal je Ereignis.
|
* **Push-Benachrichtigungen** auf Handy und Desktop (auch bei geschlossener App; iPhone/iPad als Home-Bildschirm-App), als eigener Kanal je Ereignis.
|
||||||
* **Alarme & Benachrichtigungen:** Fälligkeits-Alarme sofort per E-Mail, Discord oder eigenem Webhook; granulare Benachrichtigungen für Freigaben, erledigte geteilte Aufgaben und beobachtete Aufgaben sowie tägliche Zusammenfassungen zur gewählten Uhrzeit.
|
* **Alarme & Benachrichtigungen:** Fälligkeits-Alarme sofort, alle übrigen Ereignisse (Freigaben, erledigte geteilte Aufgaben, beobachtete Aufgaben) gebündelt; pro Ereignis wählbar über E-Mail, Discord, eigenen Webhook und Push.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="screenshots/aufgaben_desktop.png" alt="MindSift Aufgaben und Kanban-Board" width="90%" />
|
<img src="screenshots/aufgaben_desktop.png" alt="MindSift Aufgaben und Kanban-Board" width="90%" />
|
||||||
@@ -62,28 +67,32 @@ Die meisten Notiz- und Aufgaben-Apps verlangen Struktur im falschen Moment: Wenn
|
|||||||
### 📝 Workspace: Outliner-Notizen
|
### 📝 Workspace: Outliner-Notizen
|
||||||
|
|
||||||
* Jeder Gedanke ein Bullet: `Enter`, `Tab`/`Shift+Tab`, Ein-/Ausklappen, Drag & Drop ganzer Äste.
|
* Jeder Gedanke ein Bullet: `Enter`, `Tab`/`Shift+Tab`, Ein-/Ausklappen, Drag & Drop ganzer Äste.
|
||||||
* **Zoom-Fokus** per Klick auf den Bullet, Breadcrumbs zum Zurücknavigieren.
|
* **Zoom-Fokus** per Klick auf den Bullet, Breadcrumbs zum Zurücknavigieren; **Fokus-Modus** (`Strg+Umschalt+F`) zeigt nur die Notiz in Lesebreite, Ordner gleiten bei Bedarf von links herein.
|
||||||
* **Journal:** eine Notiz pro Tag, alle Tage untereinander (heute oben, ältere laden beim Scrollen nach), mit Kalender zum Springen; einzelne Tage lassen sich teilen. Pro Gerät abschaltbar.
|
* **Journal:** eine Notiz pro Tag, alle Tage untereinander (heute oben, ältere laden beim Scrollen nach), mit Kalender zum Springen; einzelne Tage lassen sich teilen. Pro Gerät abschaltbar.
|
||||||
* Blocktypen und Slash-Befehle: `/todo`, `/number` (nummerierte Liste: 1., a., i.), `/link`, `/h1`–`/h3`, `/code` (mit Syntax-Hervorhebung), `/callout`, `/divider`, `/extract`, `/image`, `/audio`, `/diktat`.
|
* Blocktypen und Slash-Befehle: `/todo`, `/number` (nummerierte Liste: 1., a., i.), `/link`, `/h1`–`/h3`, `/code` (mit Syntax-Hervorhebung), `/callout`, `/divider`, `/extract`, `/image`, `/audio`, `/diktat`.
|
||||||
* **Bilder & Audio in Notizen:** `/image` (Datei, Kamera, Zwischenablage oder Link), `/audio` (Sprachnotiz oder Datei-Upload) und `/diktat` (Live-Transkription).
|
* **Bilder & Audio in Notizen:** `/image` (Datei, Kamera, Zwischenablage oder Link), `/audio` (Sprachnotiz oder Datei-Upload) und `/diktat` (Live-Transkription).
|
||||||
* **Link-Vorschau:** Automatische Vorschaukarten über Punkten mit reiner Web-Adresse.
|
* **Link-Vorschau:** Vorschaukarte (Titel, Beschreibung, Bild) für Punkte, die nur aus einer Web-Adresse bestehen, z. B. über `/link`; die Seite ruft der Server ab, nicht dein Browser. Pro Gerät abschaltbar.
|
||||||
* **Ast auslagern & Notizen zusammenführen:** Bulletpoints samt Unterbaum in eigenständige Notizen auslagern (`/extract`) oder Notizen per Drag & Drop bzw. Menü zusammenführen (jeweils mit vollständiger Rückgängig-Funktion).
|
* **Ast auslagern & Notizen zusammenführen:** Bulletpoints samt Unterbaum in eigenständige Notizen auslagern (`/extract`) oder Notizen per Drag & Drop bzw. Menü zusammenführen (jeweils mit vollständiger Rückgängig-Funktion).
|
||||||
* Wiki-Links `[[Notiz]]`, auch auf **einzelne Punkte** (`[[Notiz#^id|Text]]`, kopierbar an jedem Punkt), Links und `#Hashtags`; Tag-Seiten bündeln alle Fundstellen.
|
* Wiki-Links `[[Notiz]]`, auch auf **einzelne Punkte** (`[[Notiz#^id|Text]]`, kopierbar an jedem Punkt), Links und `#Hashtags`; Tag-Seiten bündeln alle Fundstellen.
|
||||||
* Ordner, Favoriten, **To-Do-Rollup** über alle Notizen und Export einer Notiz als `.md`.
|
* Ordner, Favoriten, **To-Do-Rollup** über alle Notizen und Export einer Notiz als `.md`.
|
||||||
* Einfügen von mehrzeiligem Markdown wird in Blöcke zerlegt; `Strg+Z` / `Strg+Umschalt+Z` nimmt eigene Änderungen zurück bzw. stellt sie wieder her.
|
* Eingefügtes Markdown wird zur Gliederung: Überschriften werden zu Abschnitten mit eingerücktem Inhalt, Listen (auch nummeriert) behalten ihre Ebenen, Absätze werden eigene Punkte. Kürzel beim Tippen: `- `, `1. `, `[] `, `# `, `Strg+B`/`I`/`K`. `Strg+Z` / `Strg+Umschalt+Z` nimmt eigene Änderungen zurück bzw. stellt sie wieder her.
|
||||||
* **Teilen:** Notizen und ganze Ordner lassen sich Benutzern oder Gruppen als **Bearbeiter** oder **Leser** freigeben. Empfänger sortieren sie in ihren eigenen Baum ein.
|
* **Teilen:** Notizen und ganze Ordner lassen sich Benutzern oder Gruppen als **Bearbeiter** oder **Leser** freigeben. Empfänger sortieren sie in ihren eigenen Baum ein.
|
||||||
|
|
||||||
#### 🤝 Gemeinsam bearbeiten
|
#### 🤝 Gemeinsam bearbeiten
|
||||||
|
|
||||||
* **Live:** Mehrere Personen schreiben gleichzeitig in derselben Notiz; Änderungen erscheinen bei den anderen in Bruchteilen einer Sekunde. Der Kopf der Notiz zeigt, wer gerade mitliest.
|
* **Live:** Mehrere Personen schreiben gleichzeitig in derselben Notiz; Änderungen erscheinen bei den anderen in Bruchteilen einer Sekunde. Die Leiste über der Notiz zeigt, wer gerade mitliest.
|
||||||
* **Kein Überschreiben:** Wer in einem Bullet schreibt, hält ihn kurz für sich; andere sehen ein Schloss mit Namen und arbeiten in den übrigen Bullets weiter. Kommt es doch zu einem Konflikt (z. B. nach Offline-Arbeit), bleibt der zweite Text als markierte Kopie mit „Übernehmen“/„Verwerfen“ erhalten.
|
* **Kein Überschreiben:** Wer in einem Bullet schreibt, hält ihn kurz für sich; andere sehen ein Schloss mit Namen und arbeiten in den übrigen Bullets weiter. Kommt es doch zu einem Konflikt (z. B. nach Offline-Arbeit), bleibt der zweite Text als markierte Kopie mit „Übernehmen“/„Verwerfen“ erhalten.
|
||||||
* **Offline-fest:** Jede Eingabe wird zuerst im Browser gesichert und später übertragen, auch nach Neuladen oder Tagen ohne Netz.
|
* **Offline-fest:** Jede Eingabe wird zuerst im Browser gesichert und später übertragen, auch nach Neuladen oder Tagen ohne Netz.
|
||||||
* **Hinter strengen Proxys:** WebSocket → SSE → Long-Polling → Abfrage alle 3 s; MindSift wählt automatisch, was durchkommt. Der aktive Weg steht im Editor („Live“, „Abgleich alle 3 s“).
|
* **Hinter strengen Proxys:** WebSocket → SSE → Long-Polling → Abfrage alle 3 s; MindSift wählt automatisch, was durchkommt. Der aktive Weg steht in der Leiste über der Notiz („Live“, „Abgleich alle 3 s“).
|
||||||
|
|
||||||
Technik dahinter: [Doku/modules/collaboration.md](Doku/modules/collaboration.md).
|
Technik dahinter: [Doku/modules/collaboration.md](Doku/modules/collaboration.md).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="screenshots/workspace_desktop.png" alt="MindSift Workspace Outliner" width="90%" />
|
<img src="screenshots/workspace_desktop.png" alt="MindSift Workspace: zwei Personen in derselben Notiz" width="90%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="screenshots/journal_desktop.png" alt="MindSift Journal mit Kalender" width="90%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
### 🛒 Geteilte Listen
|
### 🛒 Geteilte Listen
|
||||||
@@ -99,13 +108,18 @@ Technik dahinter: [Doku/modules/collaboration.md](Doku/modules/collaboration.md)
|
|||||||
|
|
||||||
* Geführte Ersteinrichtung, Rollen *Admin* und *Mitglied*, Einladungscodes, optional offene Registrierung.
|
* Geführte Ersteinrichtung, Rollen *Admin* und *Mitglied*, Einladungscodes, optional offene Registrierung.
|
||||||
* Gruppen mit Gruppen-Admins für Freigaben.
|
* Gruppen mit Gruppen-Admins für Freigaben.
|
||||||
* Admin-Bereich: Benutzer, Einladungen, Instanzname, SMTP, Freigabe-Modus.
|
* Admin-Bereich: Benutzer, Einladungen, Instanzname, SMTP, Freigabe-Modus, Sicherungen, Spracherkennung.
|
||||||
* Fünf Farbfamilien mit je einer hellen und dunklen Variante (Klassik, Fjord, Terrakotta, Polarized, Suri), Hell/Dunkel auf Wunsch wie das System, eigene Akzentfarbe, Ecken rund oder eckig, Knöpfe mit oder ohne Rahmen plus Theme-Manager für eigene CSS-Themes, installierbar als PWA. Die App startet auch ohne Netz mit dem zuletzt geladenen Stand; offline erfasste Gedanken und abgehakte Aufgaben oder Listeneinträge werden nachgereicht, sobald die Verbindung wieder da ist.
|
|
||||||
|
### 🎨 Darstellung & App
|
||||||
|
|
||||||
|
* **Fünf Farbfamilien** mit je einer hellen und dunklen Variante: Klassik, Fjord, Terrakotta, Polarized, Suri. Hell/Dunkel auf Wunsch „Wie System“, dazu eine eigene Akzentfarbe, Ecken rund oder eckig und Knöpfe mit oder ohne Rahmen (pro Gerät). Für eigene Themes gibt es einen Theme-Manager (eine CSS-Datei).
|
||||||
|
* **Installierbar als App (PWA)** auf Smartphone und Desktop. Sie startet auch ohne Netz mit dem zuletzt geladenen Stand; offline erfasste Gedanken und abgehakte Aufgaben oder Listeneinträge werden nachgereicht, sobald die Verbindung wieder da ist.
|
||||||
|
|
||||||
### 🔌 Integrationen
|
### 🔌 Integrationen
|
||||||
|
|
||||||
* **Webhook / API-Key:** `POST /api/v1/webhooks/ingest` legt Aufgaben aus Smarthome, Skripten oder Automationen an.
|
* **Eingehend (API-Key):** `POST /api/v1/webhooks/ingest` legt Gedanken, Aufgaben oder Listeneinträge aus Smarthome, Skripten oder Automationen an.
|
||||||
* Persönliche API-Keys, Datenexport als JSON.
|
* **Ausgehend:** eigener Webhook als Benachrichtigungskanal (JSON per POST), Discord, E-Mail, Push.
|
||||||
|
* Persönliche API-Keys, Datenexport als JSON, Markdown-Archiv (Obsidian-kompatibel).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -138,7 +152,7 @@ Minimal:
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mindsift:
|
mindsift:
|
||||||
image: git.dsit.me/sandrodach/mindsift:stable # oder feste Version (:v0.8.0); :latest = main, :dev = Entwicklungsstand
|
image: git.dsit.me/sandrodach/mindsift:latest # :latest = Stand von main (jede Version), :dev = Entwicklungsstand
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports: ["8080:8080"]
|
ports: ["8080:8080"]
|
||||||
volumes: ["./data:/data"]
|
volumes: ["./data:/data"]
|
||||||
@@ -172,17 +186,19 @@ Rufe `http://localhost:8080` auf. Beim ersten Start führt ein Assistent durch d
|
|||||||
| `SMTP_FROM_NAME` | `MindSift` | Anzeigename des Absenders bei E-Mails |
|
| `SMTP_FROM_NAME` | `MindSift` | Anzeigename des Absenders bei E-Mails |
|
||||||
| `SMTP_SKIP_VERIFY` | `false` | Zertifikatsprüfung für interne Mailserver abschalten |
|
| `SMTP_SKIP_VERIFY` | `false` | Zertifikatsprüfung für interne Mailserver abschalten |
|
||||||
| `NOTIFY_DIGEST_MINUTES` | `5` | Bündelungsintervall für Benachrichtigungen in Minuten |
|
| `NOTIFY_DIGEST_MINUTES` | `5` | Bündelungsintervall für Benachrichtigungen in Minuten |
|
||||||
|
| `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `VAPID_SUBJECT` | automatisch | Schlüssel für Push-Benachrichtigungen; ohne Angabe einmal erzeugt und in der Datenbank gespeichert. `VAPID_SUBJECT` (Kontakt für die Push-Dienste, `mailto:` oder `https://`) sonst aus `BASE_URL` |
|
||||||
| `MEDIA_*` | diverse | Grenzen für Bilder und Audio in Notizen (`MEDIA_MAX_IMAGE_MB=15`, `MEDIA_MAX_AUDIO_MB=50`, `MEDIA_QUOTA_MB=0`) |
|
| `MEDIA_*` | diverse | Grenzen für Bilder und Audio in Notizen (`MEDIA_MAX_IMAGE_MB=15`, `MEDIA_MAX_AUDIO_MB=50`, `MEDIA_QUOTA_MB=0`) |
|
||||||
| `STT_*` | diverse | Optionale eigene Spracherkennung per Whisper (`STT_URL`, `STT_MODEL`, `STT_LANGUAGE` …; siehe [Doku/architecture.md](Doku/architecture.md#spracherkennung)) |
|
| `STT_*` | diverse | Optionale eigene Spracherkennung per Whisper (`STT_URL`, `STT_MODEL`, `STT_LANGUAGE` …; siehe [Doku/architecture.md](Doku/architecture.md#spracherkennung)) |
|
||||||
| `LEGAL_IMPRINT_URL`, `LEGAL_PRIVACY_URL` | leer | Impressum/Datenschutz als externer Link (nur `http(s)`) |
|
| `LEGAL_IMPRINT_URL`, `LEGAL_PRIVACY_URL` | leer | Impressum/Datenschutz als externer Link (nur `http(s)`) |
|
||||||
| `LEGAL_IMPRINT_CONTENT`, `LEGAL_PRIVACY_CONTENT` (oder `…_CONTENT_FILE` mit Dateipfad) | leer | Impressum/Datenschutz als Markdown, angezeigt in einem Lesefenster. Ohne URL und Text bleibt der Link unsichtbar. |
|
| `LEGAL_IMPRINT_CONTENT`, `LEGAL_PRIVACY_CONTENT` (oder `…_CONTENT_FILE` mit Dateipfad) | leer | Impressum/Datenschutz als Markdown, angezeigt in einem Lesefenster. Ohne URL und Text bleibt der Link unsichtbar. |
|
||||||
|
| `LEGAL_CUSTOM_LINK_LABEL`, `LEGAL_CUSTOM_LINK_URL` | leer | Zusätzlicher Link im Footer (z. B. Statusseite), nur `https` |
|
||||||
| `MINDA_DEMO` | `false` | Legt Demo-Konten `admin/admin` (mit Beispieldaten) und `user/user` an; eine Notiz und zwei Listen sind zwischen beiden geteilt, um das gemeinsame Bearbeiten auszuprobieren. **Nie auf öffentlichen Servern!** |
|
| `MINDA_DEMO` | `false` | Legt Demo-Konten `admin/admin` (mit Beispieldaten) und `user/user` an; eine Notiz und zwei Listen sind zwischen beiden geteilt, um das gemeinsame Bearbeiten auszuprobieren. **Nie auf öffentlichen Servern!** |
|
||||||
| `DEMO_RESET_MINUTES` | `20` | Nur im Demo-Modus: setzt die Datenbank in diesem Abstand auf die Beispieldaten zurück (`0` = nie). Angemeldete bleiben angemeldet, die App zeigt einen Countdown und lädt danach neu. |
|
| `DEMO_RESET_MINUTES` | `20` | Nur im Demo-Modus: setzt die Datenbank in diesem Abstand auf die Beispieldaten zurück (`0` = nie). Angemeldete bleiben angemeldet, die App zeigt einen Countdown und lädt danach neu. |
|
||||||
|
|
||||||
* **Reverse-Proxy:** Am besten WebSocket-Upgrade und ungepufferte SSE-Streams durchreichen. Blockiert ein Proxy beides, funktioniert MindSift trotzdem über Long-Polling bzw. regelmäßige Abfragen, nur etwas träger. Beispiele für Nginx, Traefik, Caddy und Cloudflare: [Doku/reverse-proxy.md](Doku/reverse-proxy.md).
|
* **Reverse-Proxy:** Am besten WebSocket-Upgrade und ungepufferte SSE-Streams durchreichen. Blockiert ein Proxy beides, funktioniert MindSift trotzdem über Long-Polling bzw. regelmäßige Abfragen, nur etwas träger. Beispiele für Nginx, Traefik, Caddy und Cloudflare: [Doku/reverse-proxy.md](Doku/reverse-proxy.md).
|
||||||
* **Datensicherung:** In den Admin-Einstellungen lassen sich automatische tägliche Backups einschalten (ganze Datenbank plus ein Backup je Benutzer unter `data/backups/`, Standard 3 Tage aufbewahrt). Benutzer laden ihre Server-Backups herunter oder spielen sie ganz oder für einzelne Bereiche wieder ein. Der Ordner liegt neben der Datenbank: zusätzlich woanders sichern. Manuell geht weiterhin `sqlite3 mindsift.db ".backup 'backup.db'"`.
|
* **Datensicherung:** In den Admin-Einstellungen lassen sich automatische tägliche Backups einschalten (ganze Datenbank plus ein Backup je Benutzer unter `data/backups/`, Standard 3 Tage aufbewahrt). Benutzer laden ihre Server-Backups herunter oder spielen sie ganz oder für einzelne Bereiche wieder ein. Der Ordner liegt neben der Datenbank: zusätzlich woanders sichern. Manuell geht weiterhin `sqlite3 mindsift.db ".backup 'backup.db'"`.
|
||||||
* **Updates:** Neues Image ziehen und Container neu starten (`docker compose pull && docker compose up -d`). Das Schema wird beim Start automatisch angepasst. Die laufende Version steht im Kopf der App unter dem Namen.
|
* **Updates:** Neues Image ziehen und Container neu starten (`docker compose pull && docker compose up -d`). Das Schema wird beim Start automatisch angepasst. Die laufende Version steht im Kopf der App unter dem Namen.
|
||||||
* **Image-Tags:** `:dev` = Stand des Branches `dev`, `:latest` = Stand von `main`, `:stable` = zuletzt veröffentlichte Version. Ein Git-Tag wie `v1.2.0` erzeugt `:v1.2.0` und verschiebt `:stable` darauf; Vorabversionen wie `v1.0.0-rc.1` verschieben `:stable` nicht.
|
* **Image-Tags:** `:dev` = Stand des Branches `dev`, `:latest` = Stand von `main` (jede neue Version landet dort). Ein Git-Tag wie `v1.2.0` erzeugt zusätzlich `:v1.2.0` und `:stable`; Vorabversionen wie `v1.0.0-rc.1` verschieben `:stable` nicht.
|
||||||
* **Versionen & Änderungen:** [Semantic Versioning](https://semver.org/spec/v2.0.0.html), was sich geändert hat, steht in der [CHANGELOG.md](CHANGELOG.md).
|
* **Versionen & Änderungen:** [Semantic Versioning](https://semver.org/spec/v2.0.0.html), was sich geändert hat, steht in der [CHANGELOG.md](CHANGELOG.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -204,12 +220,12 @@ npm run test:run # Komponententests (Vitest, jsdom)
|
|||||||
npm run build # Produktions-Build
|
npm run build # Produktions-Build
|
||||||
cd ..
|
cd ..
|
||||||
e2e/run.sh # Browser-Tests (Playwright, zwei Benutzer) gegen ein frisch gebautes Image
|
e2e/run.sh # Browser-Tests (Playwright, zwei Benutzer) gegen ein frisch gebautes Image
|
||||||
APP_VERSION=v0.4 e2e/screenshots.sh # Screenshots dieser README neu erzeugen (Demo-Daten)
|
e2e/screenshots.sh # Screenshots dieser README neu erzeugen (Demo-Daten, hell/dunkel diagonal)
|
||||||
```
|
```
|
||||||
|
|
||||||
Für das Binary wird das gebaute Frontend nach `cmd/server/dist` kopiert (`go:embed`); das mehrstufige `Dockerfile` erledigt das und führt Typprüfung, Frontend-Tests, Go-Vet und Go-Tests beim Image-Build aus.
|
Für das Binary wird das gebaute Frontend nach `cmd/server/dist` kopiert (`go:embed`); das mehrstufige `Dockerfile` erledigt das und führt Typprüfung, Frontend-Tests, Go-Vet und Go-Tests beim Image-Build aus.
|
||||||
|
|
||||||
**Stack:** Go (chi, gorilla/websocket, JWT, modernc SQLite) · React 18, TypeScript, Vite, Tailwind.
|
**Stack:** Go (chi, gorilla/websocket, JWT, modernc SQLite, webpush-go) · React 18, TypeScript, Vite, Tailwind.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -221,7 +237,7 @@ Die technische Doku liegt unter [Doku/](Doku/README.md): Architektur, Datenbank,
|
|||||||
|
|
||||||
## 🔒 Deine Daten gehören dir
|
## 🔒 Deine Daten gehören dir
|
||||||
|
|
||||||
* Selbst gehostet, keine Telemetrie, keine Fremddienste. Ausgehende Verbindungen gibt es nur zu den Alarm-Zielen, die du selbst einrichtest (SMTP, Discord, Webhook).
|
* Selbst gehostet, keine Telemetrie, keine Fremddienste, Schrift und Icons werden mitgeliefert. Ausgehende Verbindungen baut nur der Server auf, und nur für das, was du nutzt: Benachrichtigungen (SMTP, Discord, eigener Webhook, die Push-Dienste der Browser), Link-Vorschauen (die verlinkte Seite wird einmal abgerufen, pro Gerät abschaltbar) und optional dein eigener Whisper-Server.
|
||||||
* Alle Inhalte liegen in einer einzigen SQLite-Datei; Bilder und Sprachnotizen liegen daneben in `data/media/`. Der ganze Workspace lässt sich als Obsidian-kompatibles Markdown-Archiv (ZIP, mit `attachments/`) exportieren, alle Daten eines Benutzers als Backup-ZIP (JSON plus Mediendateien).
|
* Alle Inhalte liegen in einer einzigen SQLite-Datei; Bilder und Sprachnotizen liegen daneben in `data/media/`. Der ganze Workspace lässt sich als Obsidian-kompatibles Markdown-Archiv (ZIP, mit `attachments/`) exportieren, alle Daten eines Benutzers als Backup-ZIP (JSON plus Mediendateien).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
// Takes the README screenshots from a fresh server in demo mode (sample data of admin/admin).
|
// Takes the README screenshots from a fresh server in demo mode (sample data of admin/admin).
|
||||||
|
// Every picture is taken twice in the same state – light and dark theme – and the two halves are
|
||||||
|
// joined along a slanted diagonal (light left, dark right).
|
||||||
// Usage: e2e/screenshots.sh (builds the image) or MINDSIFT_URL=… SHOTS_DIR=… node screenshots.mjs
|
// Usage: e2e/screenshots.sh (builds the image) or MINDSIFT_URL=… SHOTS_DIR=… node screenshots.mjs
|
||||||
|
// Theme pair: SHOT_LIGHT / SHOT_DARK (default: light / dark, the "Klassik" family)
|
||||||
|
import fs from 'node:fs';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
const BASE = process.env.MINDSIFT_URL || 'http://localhost:18080';
|
const BASE = process.env.MINDSIFT_URL || 'http://localhost:18080';
|
||||||
const OUT = process.env.SHOTS_DIR || '../screenshots';
|
const OUT = process.env.SHOTS_DIR || '../screenshots';
|
||||||
|
const LIGHT = process.env.SHOT_LIGHT || 'light';
|
||||||
|
const DARK = process.env.SHOT_DARK || 'dark';
|
||||||
const DESKTOP = { viewport: { width: 1518, height: 886 }, deviceScaleFactor: 2 };
|
const DESKTOP = { viewport: { width: 1518, height: 886 }, deviceScaleFactor: 2 };
|
||||||
const MOBILE = { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true };
|
const MOBILE = { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true };
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
@@ -20,7 +26,13 @@ async function login(user) {
|
|||||||
|
|
||||||
async function open(browser, token, options) {
|
async function open(browser, token, options) {
|
||||||
const ctx = await browser.newContext({ ...options, colorScheme: 'dark', locale: 'de-DE' });
|
const ctx = await browser.newContext({ ...options, colorScheme: 'dark', locale: 'de-DE' });
|
||||||
await ctx.addInitScript((t) => localStorage.setItem('mindsift_jwt_token', t), token);
|
await ctx.addInitScript(
|
||||||
|
([t, theme]) => {
|
||||||
|
localStorage.setItem('mindsift_jwt_token', t);
|
||||||
|
localStorage.setItem('mindsift_active_theme', theme);
|
||||||
|
},
|
||||||
|
[token, DARK]
|
||||||
|
);
|
||||||
const page = await ctx.newPage();
|
const page = await ctx.newPage();
|
||||||
page.on('pageerror', (e) => console.log('PAGEERROR', e.message));
|
page.on('pageerror', (e) => console.log('PAGEERROR', e.message));
|
||||||
await page.goto(BASE);
|
await page.goto(BASE);
|
||||||
@@ -28,12 +40,76 @@ async function open(browser, token, options) {
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tab = (page, name) => page.getByRole('button', { name: new RegExp(name) }).first().click();
|
// Anchored, so "Workspace" does not hit the triage button "Als Notiz – Im Workspace"
|
||||||
|
const tab = (page, name) => page.getByRole('button', { name: new RegExp(`^${name}`) }).locator('visible=true').first().click();
|
||||||
|
|
||||||
|
// Switches the theme in place (no reload), so both halves show exactly the same state
|
||||||
|
async function setTheme(page, id, dark) {
|
||||||
|
await page.evaluate(
|
||||||
|
([themeId, isDark]) => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.setAttribute('data-theme', themeId);
|
||||||
|
root.classList.toggle('dark', isDark);
|
||||||
|
},
|
||||||
|
[id, dark]
|
||||||
|
);
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
let composer;
|
||||||
|
// Light picture on the left of the diagonal, dark on the right, a thin line between them
|
||||||
|
async function compose(lightPng, darkPng, file) {
|
||||||
|
const dataUrl = await composer.evaluate(
|
||||||
|
async ([light, dark]) => {
|
||||||
|
const load = (b64) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.src = 'data:image/png;base64,' + b64;
|
||||||
|
});
|
||||||
|
const [a, b] = await Promise.all([load(light), load(dark)]);
|
||||||
|
const w = a.naturalWidth;
|
||||||
|
const h = a.naturalHeight;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = w;
|
||||||
|
canvas.height = h;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(b, 0, 0);
|
||||||
|
const top = w * 0.58;
|
||||||
|
const bottom = w * 0.42;
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, 0);
|
||||||
|
ctx.lineTo(top, 0);
|
||||||
|
ctx.lineTo(bottom, h);
|
||||||
|
ctx.lineTo(0, h);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.clip();
|
||||||
|
ctx.drawImage(a, 0, 0);
|
||||||
|
ctx.restore();
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
||||||
|
ctx.lineWidth = Math.max(2, w / 600);
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.35)';
|
||||||
|
ctx.shadowBlur = w / 300;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(top, 0);
|
||||||
|
ctx.lineTo(bottom, h);
|
||||||
|
ctx.stroke();
|
||||||
|
return canvas.toDataURL('image/png');
|
||||||
|
},
|
||||||
|
[lightPng.toString('base64'), darkPng.toString('base64')]
|
||||||
|
);
|
||||||
|
fs.writeFileSync(`${OUT}/${file}`, Buffer.from(dataUrl.split(',')[1], 'base64'));
|
||||||
|
}
|
||||||
|
|
||||||
async function shot(page, file) {
|
async function shot(page, file) {
|
||||||
await sleep(800); // animations
|
await sleep(800); // animations
|
||||||
await page.mouse.move(0, 0);
|
await page.mouse.move(0, 0);
|
||||||
await page.screenshot({ path: `${OUT}/${file}` });
|
await setTheme(page, LIGHT, false);
|
||||||
|
const light = await page.screenshot();
|
||||||
|
await setTheme(page, DARK, true);
|
||||||
|
const dark = await page.screenshot();
|
||||||
|
await compose(light, dark, file);
|
||||||
console.log(`OK ${file}`);
|
console.log(`OK ${file}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +117,7 @@ const main = async () => {
|
|||||||
const admin = await login('admin');
|
const admin = await login('admin');
|
||||||
const user = await login('user');
|
const user = await login('user');
|
||||||
const browser = await chromium.launch({ args: ['--lang=de-DE'] });
|
const browser = await chromium.launch({ args: ['--lang=de-DE'] });
|
||||||
|
composer = await (await browser.newContext()).newPage();
|
||||||
|
|
||||||
const page = await open(browser, admin, DESKTOP);
|
const page = await open(browser, admin, DESKTOP);
|
||||||
await tab(page, 'Triage');
|
await tab(page, 'Triage');
|
||||||
@@ -67,6 +144,36 @@ const main = async () => {
|
|||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
await shot(page, 'workspace_desktop.png');
|
await shot(page, 'workspace_desktop.png');
|
||||||
|
|
||||||
|
// Journal: a few days with entries, the calendar in the sidebar
|
||||||
|
const H = { Authorization: `Bearer ${admin}`, 'Content-Type': 'application/json' };
|
||||||
|
const today = new Date();
|
||||||
|
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
|
const day = (offset) => iso(new Date(today.getFullYear(), today.getMonth(), today.getDate() - offset));
|
||||||
|
await fetch(`${BASE}/api/v1/journal/${day(0)}`, { method: 'POST', headers: H });
|
||||||
|
await page.getByRole('button', { name: 'Journal', exact: true }).click();
|
||||||
|
await sleep(1500);
|
||||||
|
const write = async (date, lines) => {
|
||||||
|
const section = page.locator(`[data-journal-date="${date}"]`);
|
||||||
|
if ((await section.count()) === 0) return; // day outside the visible month
|
||||||
|
await section.locator('.cursor-text').first().click();
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (i > 0) await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type(lines[i], { delay: 5 });
|
||||||
|
}
|
||||||
|
await sleep(600);
|
||||||
|
};
|
||||||
|
await write(day(0), ['Standup: Umzug liegt im Plan #homelab', 'Nachmittags Backup-Test mit restic', 'Idee: Wochenrückblick als Vorlage anlegen']);
|
||||||
|
for (const offset of [2, 5]) {
|
||||||
|
await page.getByTitle(new RegExp(new Date(today.getFullYear(), today.getMonth(), today.getDate() - offset).toLocaleDateString('de-DE', { day: 'numeric', month: 'long' }))).first().click().catch(() => {});
|
||||||
|
await sleep(1200);
|
||||||
|
await write(day(offset), offset === 2 ? ['Rasen gemäht, Hecke wartet noch #garten'] : ['Erste Gedanken zur Hausrenovierung #renovierung', 'Angebote einholen bis Monatsende']);
|
||||||
|
}
|
||||||
|
await page.getByRole('button', { name: 'Heute' }).click().catch(() => {});
|
||||||
|
await page.mouse.click(1000, 860);
|
||||||
|
await sleep(2500);
|
||||||
|
await shot(page, 'journal_desktop.png');
|
||||||
|
await page.getByRole('button', { name: 'Journal', exact: true }).click(); // close it again
|
||||||
|
|
||||||
await tab(page, 'Listen');
|
await tab(page, 'Listen');
|
||||||
await page.getByText('Wocheneinkauf', { exact: true }).first().click().catch(() => {});
|
await page.getByText('Wocheneinkauf', { exact: true }).first().click().catch(() => {});
|
||||||
await shot(page, 'Listen_desktop.png');
|
await shot(page, 'Listen_desktop.png');
|
||||||
@@ -75,6 +182,12 @@ const main = async () => {
|
|||||||
await mobile.getByText('Später im Browser weiter nutzen').click({ timeout: 5000 }).catch(() => {}); // install prompt
|
await mobile.getByText('Später im Browser weiter nutzen').click({ timeout: 5000 }).catch(() => {}); // install prompt
|
||||||
await shot(mobile, 'Mobile_Triage.png');
|
await shot(mobile, 'Mobile_Triage.png');
|
||||||
|
|
||||||
|
await tab(mobile, 'Workspace');
|
||||||
|
await sleep(600);
|
||||||
|
await mobile.getByText(/^Homelab 2\.0/).first().click();
|
||||||
|
await sleep(1500);
|
||||||
|
await shot(mobile, 'Mobile_Workspace.png');
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,36 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Builds MindSift, starts it in demo mode with a fresh database and renews the README screenshots
|
# Builds MindSift, starts it in demo mode with a fresh database and renews the README screenshots
|
||||||
# in screenshots/. Requires podman (or docker via ENGINE=docker). Usage: e2e/screenshots.sh
|
# in screenshots/ (light and dark theme joined along a diagonal). Requires podman (or docker via
|
||||||
|
# ENGINE=docker). Usage: e2e/screenshots.sh, other theme pair: SHOT_LIGHT=fjord SHOT_DARK=fjord-night
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ENGINE="${ENGINE:-podman}"
|
ENGINE="${ENGINE:-podman}"
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
WORK="$(mktemp -d)"
|
WORK="$(mktemp -d)"
|
||||||
trap '"$ENGINE" rm -f mindsift-shots >/dev/null 2>&1 || true; rm -rf "$WORK"' EXIT
|
RUN_ID="$$-$RANDOM"
|
||||||
|
NAME="mindsift-shots-$RUN_ID"
|
||||||
|
IMAGE="mindsift-shots-image:$RUN_ID"
|
||||||
|
PORT="${SHOTS_PORT:-}"
|
||||||
|
if [ -z "$PORT" ]; then
|
||||||
|
for p in $(shuf -i 18100-18999 -n 50); do
|
||||||
|
if ! (exec 3<>"/dev/tcp/127.0.0.1/$p") 2>/dev/null; then PORT="$p"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
cleanup() {
|
||||||
|
"$ENGINE" rm -f "$NAME" >/dev/null 2>&1 || true
|
||||||
|
"$ENGINE" rmi "$IMAGE" >/dev/null 2>&1 || true
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
echo "Baue Image …"
|
echo "Baue Image …"
|
||||||
# Version in the header, e.g. APP_VERSION=v0.2 e2e/screenshots.sh
|
# Version in the header, e.g. APP_VERSION=v0.2 e2e/screenshots.sh
|
||||||
"$ENGINE" build -q --build-arg APP_VERSION="${APP_VERSION:-}" -t mindsift-e2e-image "$ROOT" >/dev/null
|
"$ENGINE" build -q --build-arg APP_VERSION="${APP_VERSION:-}" -t "$IMAGE" "$ROOT" >/dev/null
|
||||||
"$ENGINE" run -d --name mindsift-shots --network host -e PORT=18081 -e MINDA_DEMO=true \
|
"$ENGINE" run -d --name "$NAME" --network host -e PORT="$PORT" -e MINDA_DEMO=true -e DEMO_RESET_MINUTES=0 \
|
||||||
-e JWT_SECRET=screenshots-only-secret -v "$WORK":/data:Z mindsift-e2e-image >/dev/null
|
-e JWT_SECRET=screenshots-only-secret -v "$WORK":/data:Z "$IMAGE" >/dev/null
|
||||||
for _ in $(seq 1 60); do curl -s -m 1 http://localhost:18081/api/v1/health >/dev/null && break; sleep 0.5; done
|
for _ in $(seq 1 60); do curl -s -m 1 "http://localhost:$PORT/api/v1/health" >/dev/null && break; sleep 0.5; done
|
||||||
|
|
||||||
echo "Erstelle Screenshots …"
|
echo "Erstelle Screenshots …"
|
||||||
"$ENGINE" run --rm --network host -e MINDSIFT_URL=http://localhost:18081 -e SHOTS_DIR=/shots -e LANG=de_DE.UTF-8 \
|
"$ENGINE" run --rm --network host -e MINDSIFT_URL="http://localhost:$PORT" -e SHOTS_DIR=/shots \
|
||||||
|
-e SHOT_LIGHT="${SHOT_LIGHT:-light}" -e SHOT_DARK="${SHOT_DARK:-dark}" -e LANG=de_DE.UTF-8 -e TZ=Europe/Berlin \
|
||||||
-v "$ROOT/e2e":/e2e:Z -v "$ROOT/screenshots":/shots:Z -w /e2e mcr.microsoft.com/playwright:v1.48.2-jammy \
|
-v "$ROOT/e2e":/e2e:Z -v "$ROOT/screenshots":/shots:Z -w /e2e mcr.microsoft.com/playwright:v1.48.2-jammy \
|
||||||
sh -c 'apt-get update -qq >/dev/null && apt-get install -y -qq fonts-inter >/dev/null 2>&1; npm install --no-audit --no-fund >/dev/null 2>&1 && node screenshots.mjs'
|
sh -c 'npm install --no-audit --no-fund >/dev/null 2>&1 && node screenshots.mjs'
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.14.3",
|
"version": "0.15.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.14.3",
|
"version": "0.15.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "5.3.0",
|
"@fontsource-variable/inter": "5.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.14.3",
|
"version": "0.15.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2102,7 +2102,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3.5">
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,18rem),1fr))] gap-3.5">
|
||||||
|
{/* As many columns as fit with at least 18rem each, so tag names and note titles have room */}
|
||||||
{filteredTags.map((t) => {
|
{filteredTags.map((t) => {
|
||||||
const meta = tagsMetadataMap.get(t.tag.toLowerCase());
|
const meta = tagsMetadataMap.get(t.tag.toLowerCase());
|
||||||
return (
|
return (
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 425 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 451 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 559 KiB |
|
After Width: | Height: | Size: 508 KiB |
|
Before Width: | Height: | Size: 362 KiB After Width: | Height: | Size: 556 KiB |