diff --git a/CHANGELOG.md b/CHANGELOG.md index 6598461..6f5fa0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [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 ### 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. - 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.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 diff --git a/README.md b/README.md index 24d4bf7..f83ce25 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,16 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.8.0-16a34a?style=flat-square)](#-schnellstart) +[![Version](https://img.shields.io/badge/Version-v0.15.0-16a34a?style=flat-square)](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 Triage Desktop + MindSift Triage am Desktop, links hell, rechts dunkel

+Alle Screenshots zeigen links das helle und rechts das dunkle Theme. + [Philosophie](#-die-philosophie) • [Funktionen](#-funktionen) • [Schnellstart](#-schnellstart) • [Betrieb](#-betrieb--konfiguration) • [Entwicklung](#-entwicklung) • [Dokumentation](#-dokumentation) @@ -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). * 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.

- MindSift Mobile Triage + MindSift Triage am Smartphone +    + MindSift Notiz am Smartphone

### ✅ 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. * **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. -* **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.

MindSift Aufgaben und Kanban-Board @@ -62,28 +67,32 @@ Die meisten Notiz- und Aufgaben-Apps verlangen Struktur im falschen Moment: Wenn ### 📝 Workspace: Outliner-Notizen * 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. * 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). -* **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). * 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`. -* 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. #### 🤝 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. * **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).

- MindSift Workspace Outliner + MindSift Workspace: zwei Personen in derselben Notiz +

+ +

+ MindSift Journal mit Kalender

### 🛒 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. * Gruppen mit Gruppen-Admins für Freigaben. -* Admin-Bereich: Benutzer, Einladungen, Instanzname, SMTP, Freigabe-Modus. -* 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. +* Admin-Bereich: Benutzer, Einladungen, Instanzname, SMTP, Freigabe-Modus, Sicherungen, Spracherkennung. + +### 🎨 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 -* **Webhook / API-Key:** `POST /api/v1/webhooks/ingest` legt Aufgaben aus Smarthome, Skripten oder Automationen an. -* Persönliche API-Keys, Datenexport als JSON. +* **Eingehend (API-Key):** `POST /api/v1/webhooks/ingest` legt Gedanken, Aufgaben oder Listeneinträge aus Smarthome, Skripten oder Automationen an. +* **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 services: 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 ports: ["8080:8080"] 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_SKIP_VERIFY` | `false` | Zertifikatsprüfung für interne Mailserver abschalten | | `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`) | | `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_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!** | | `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). * **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. -* **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). --- @@ -204,12 +220,12 @@ npm run test:run # Komponententests (Vitest, jsdom) npm run build # Produktions-Build cd .. 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. -**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 -* 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). --- diff --git a/e2e/screenshots.mjs b/e2e/screenshots.mjs index d8da61d..e5dc4c5 100644 --- a/e2e/screenshots.mjs +++ b/e2e/screenshots.mjs @@ -1,9 +1,15 @@ // 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 +// Theme pair: SHOT_LIGHT / SHOT_DARK (default: light / dark, the "Klassik" family) +import fs from 'node:fs'; import { chromium } from 'playwright'; const BASE = process.env.MINDSIFT_URL || 'http://localhost:18080'; 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 MOBILE = { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); @@ -20,7 +26,13 @@ async function login(user) { async function open(browser, token, options) { 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(); page.on('pageerror', (e) => console.log('PAGEERROR', e.message)); await page.goto(BASE); @@ -28,12 +40,76 @@ async function open(browser, token, options) { 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) { await sleep(800); // animations 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}`); } @@ -41,6 +117,7 @@ const main = async () => { const admin = await login('admin'); const user = await login('user'); const browser = await chromium.launch({ args: ['--lang=de-DE'] }); + composer = await (await browser.newContext()).newPage(); const page = await open(browser, admin, DESKTOP); await tab(page, 'Triage'); @@ -67,6 +144,36 @@ const main = async () => { await sleep(2000); 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 page.getByText('Wocheneinkauf', { exact: true }).first().click().catch(() => {}); 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 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(); }; diff --git a/e2e/screenshots.sh b/e2e/screenshots.sh index 040553e..195b913 100755 --- a/e2e/screenshots.sh +++ b/e2e/screenshots.sh @@ -1,20 +1,36 @@ #!/usr/bin/env bash # 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 ENGINE="${ENGINE:-podman}" ROOT="$(cd "$(dirname "$0")/.." && pwd)" 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 …" # 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" run -d --name mindsift-shots --network host -e PORT=18081 -e MINDA_DEMO=true \ - -e JWT_SECRET=screenshots-only-secret -v "$WORK":/data:Z mindsift-e2e-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 +"$ENGINE" build -q --build-arg APP_VERSION="${APP_VERSION:-}" -t "$IMAGE" "$ROOT" >/dev/null +"$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 "$IMAGE" >/dev/null +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 …" -"$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 \ - 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' diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7e82406..d7d4581 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.14.3", + "version": "0.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.14.3", + "version": "0.15.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 8940d7a..3977dc4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.14.3", + "version": "0.15.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 474ab4e..4b5d317 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -2102,7 +2102,8 @@ export const WorkspaceView: React.FC = ({ )} ) : ( -
+
+ {/* As many columns as fit with at least 18rem each, so tag names and note titles have room */} {filteredTags.map((t) => { const meta = tagsMetadataMap.get(t.tag.toLowerCase()); return ( diff --git a/screenshots/Listen_desktop.png b/screenshots/Listen_desktop.png index f78d50d..28bfcd4 100644 Binary files a/screenshots/Listen_desktop.png and b/screenshots/Listen_desktop.png differ diff --git a/screenshots/Mobile_Triage.png b/screenshots/Mobile_Triage.png index ff50fc3..2277c1e 100644 Binary files a/screenshots/Mobile_Triage.png and b/screenshots/Mobile_Triage.png differ diff --git a/screenshots/Mobile_Workspace.png b/screenshots/Mobile_Workspace.png new file mode 100644 index 0000000..02f0640 Binary files /dev/null and b/screenshots/Mobile_Workspace.png differ diff --git a/screenshots/Triage_desktop.png b/screenshots/Triage_desktop.png index ddc1b1e..fadce1b 100644 Binary files a/screenshots/Triage_desktop.png and b/screenshots/Triage_desktop.png differ diff --git a/screenshots/aufgaben_desktop.png b/screenshots/aufgaben_desktop.png index 58e2112..7459b86 100644 Binary files a/screenshots/aufgaben_desktop.png and b/screenshots/aufgaben_desktop.png differ diff --git a/screenshots/journal_desktop.png b/screenshots/journal_desktop.png new file mode 100644 index 0000000..a71b549 Binary files /dev/null and b/screenshots/journal_desktop.png differ diff --git a/screenshots/workspace_desktop.png b/screenshots/workspace_desktop.png index 42947f6..30eb193 100644 Binary files a/screenshots/workspace_desktop.png and b/screenshots/workspace_desktop.png differ