Selbst gehostete Espresso-Shot-Verwaltung für die Decent Espresso DE1. Jeden Shot erfassen, Extraktionskurven analysieren, Profile vergleichen und Muster über die gesamte Brühhistorie entdecken — lokal gespeichert, vollständig in deiner Hand.
Die Decent DE1 ist eine außergewöhnliche Espressomaschine — aber die Decent App auf dem Tablet sammelt im Laufe der Jahre immer mehr Shot-Daten an, und eine große Historie verlangsamt den Start. Ich wollte die Shots vom Tablet archivieren, dauerhaft zugänglich behalten und auf meine eigene Weise auswerten.
Visualizer Lite entstand aus diesem Bedarf:
- Datenkontrolle — alle Daten bleiben lokal; kein Cloud-Account, keine externe Serviceabhängigkeit
- Vollständige Historie — Shots über Jahre hinweg durchsuchen, filtern und vergleichen, nicht nur die neuesten
- Tablet entlasten — nach dem Import können Shots von der DE1-Maschine gelöscht werden, damit die Decent App auf dem Tablet schnell und reaktionsfreudig bleibt
- Erweiterbarkeit — die Daten gehören dir; exportieren, analysieren, direkt abfragen
- Ein schönes Nebenprojekt — ehrlich gesagt macht die Entwicklung einfach Spaß
- Direktimport (Pull) — Shots direkt von der DE1-Maschine holen mit der Advanced REST API-Extension; kein Kabel, keine manuelle Dateiübertragung nötig
- Automatischer Upload (Push) — Shots werden nach jeder Extraktion automatisch hochgeladen über das modifizierte Upload to visualizer-DE1-Plugin aus diesem Repository
- Manueller Upload — Einzelne oder mehrere
.shot-Dateien per Drag-and-drop oder Dateiauswahl über das Web-Interface hochladen - Export — Die gesamte Shot-Sammlung als ZIP-Archiv herunterladen, zur Sicherung oder für externe Analysen
- Filterbare Shot-Liste — Suche und Filter nach Röster, Bohne, Profil, Mahlwerk, Getränketyp und mehr
- Statistik-Dashboard — KPI-Kacheln mit Periodenvergleich (24h bis Gesamt), Top-Röster/Röstungen/Profile, konfigurierbarer Getränkefilter (Espresso vs. Filter); inkl. Röster & Bohnen- und Profile-Tabs mit sortierbaren Metriktabellen
- Shot-Vergleich — Zwei Shots überlagert oder nebeneinander mit Extraktionskurven und Kennzahlen-Diff
- KI-Analyse (experimentell, zum Spaß) — On-Demand Shot-Analyse über Claude oder OpenAI: Barista-Perspektive (Brühtechnik, Mahlgrad, Tamping) und Röster-Perspektive (Bohne, Röstgrad, Frische); phasenbewusst mit stabiler Sub-Phasen-Erkennung; eigener API-Key erforderlich. Die Ergebnisse sind interessant, aber nicht verbindlich — beste Resultate mit Claude Sonnet. Jede Analyse speichert Datum, Modell, Token-Anzahl und USD-Kosten (live von OpenRouter abgerufen, dauerhaft gespeichert damit historische Kosten auch bei späteren Preisänderungen korrekt bleiben) · Technische Details →
- Self-Hosted, einzelner Container — Läuft lokal oder auf einem NAS (Synology etc.) als einzelner Docker-Container mit SQLite; keine Cloud, kein Account, volle Datenkontrolle
⚠️ Kein Multi-Tenant-Support — eine Instanz, ein Benutzer⚠️ Bewusst nicht mit der Decent/Kaffee-Community verbunden (kein Teilen, kein Leaderboard)
- DE1-Tablet entlasten — Nach dem Import in Visualizer Lite können die Shots von der DE1-Maschine gelöscht werden, damit die Decent App auf dem Tablet schneller startet
Kein Build nötig — einfach das veröffentlichte Image aus der GitHub Container Registry verwenden.
Synology NAS? Separate Anleitung: Synology-Installation →
HTTP (lokales Netzwerk):
docker run -d \
--name visualizer-lite \
--restart unless-stopped \
-p 3000:3000 \
-v /volume1/docker/visualizer-lite/data:/data \
-e VL_SESSION_SECRET="$(openssl rand -base64 48)" \
-e VL_PASSWORD="dein-passwort" \
ghcr.io/tomschmidtdev/visualizer-lite:latestParameter-Erläuterung:
Parameter Was anpassen -v /volume1/docker/visualizer-lite/data:/dataDer Pfad links vom Doppelpunkt gibt an, wo die Daten auf dem Host gespeichert werden. Beliebiges Verzeichnis wählen (z.B. /home/benutzer/visualizer-daten). Das/datarechts muss so bleiben.VL_SESSION_SECRETEin langer, zufälliger String zum Signieren von Login-Sessions. Generieren mit openssl rand -base64 48oder einem Passwort-Manager. Geheim halten und konsistent lassen — eine Änderung meldet alle aktiven Sessions ab.VL_PASSWORDDas Login-Passwort. Starkes Passwort wählen, wenn die Instanz außerhalb des Heimnetzwerks erreichbar ist. Kann später in den App-Einstellungen geändert werden. -p 3000:3000Der Zugriffsport (linke Seite). Auf z.B. -p 8080:3000ändern, wenn Port 3000 bereits belegt ist.
macOS / Windows — HTTP (lokale Nutzung):
macOS (Terminal):
docker run -d \
--name visualizer-lite \
--restart unless-stopped \
-p 3000:3000 \
-v "$HOME/visualizer-lite-data:/data" \
-e VL_SESSION_SECRET="$(openssl rand -base64 48)" \
-e VL_PASSWORD="dein-passwort" \
ghcr.io/tomschmidtdev/visualizer-lite:latestWindows (PowerShell):
docker run -d `
--name visualizer-lite `
--restart unless-stopped `
-p 3000:3000 `
-v "$env:USERPROFILE\visualizer-lite-data:/data" `
-e "VL_SESSION_SECRET=beliebigen-langen-zufallsstring-hier-eintragen" `
-e "VL_PASSWORD=dein-passwort" `
ghcr.io/tomschmidtdev/visualizer-lite:latestDanach im Browser öffnen: http://localhost:3000
Wann wird HTTPS benötigt? Wenn Visualizer Lite nur innerhalb des Heimnetzwerks genutzt wird — also im selben Netzwerk wie die DE1 und alle Geräte, mit denen auf die App zugegriffen wird — ist reines HTTP ausreichend. Der Datenverkehr verlässt das Netzwerk nicht. HTTPS ist nur nötig, wenn die Instanz von außen erreichbar sein soll (Mobilfunk, Büro, VPN). In diesem Fall entweder ein Zertifikatsverzeichnis einbinden (wie unten gezeigt) oder einen Reverse Proxy (z.B. Nginx Proxy Manager oder Synologys eingebauten Reverse Proxy) vorschalten und den Container intern auf HTTP betreiben.
HTTPS (mit Zertifikaten):
docker run -d \
--name visualizer-lite \
--restart unless-stopped \
-p 3443:3000 \
-v /volume1/docker/visualizer-lite/data:/data \
-v /volume1/docker/visualizer-lite/certs:/certs:ro \
-e VL_SESSION_SECRET="$(openssl rand -base64 48)" \
-e VL_PASSWORD="dein-passwort" \
ghcr.io/tomschmidtdev/visualizer-lite:latest| HTTP | HTTPS | |
|---|---|---|
| Zertifikat erforderlich | Nein | Ja |
| Geeignet für | Nur lokales Netzwerk | Internet / externer Zugriff |
| Port | 3000 | 3443 (oder beliebig) |
Ein Zertifikatsverzeichnis mounten — die App aktiviert HTTPS automatisch:
docker run -d \
--name visualizer-lite \
--restart unless-stopped \
-p 3443:3000 \
-v /volume1/docker/visualizer-lite/data:/data \
-v /volume1/docker/visualizer-lite/certs:/certs:ro \
-e VL_SESSION_SECRET="$(openssl rand -base64 48)" \
-e VL_PASSWORD="dein-passwort" \
visualizer-lite:nasZertifikatsdateien ablegen unter:
/volume1/docker/visualizer-lite/certs/
├── fullchain.pem
└── privkey.pem
Synology-Tipp: Zertifikat über Systemsteuerung → Sicherheit → Zertifikat → Exportieren herunterladen, dann
cat cert.pem chain.pem > fullchain.pem.
Alternativ den eingebauten Reverse Proxy nutzen (Systemsteuerung → Anmeldeportal → Erweitert) – dann läuft der Container nur auf HTTP, HTTPS übernimmt Synology.
| Variable | Standard | Beschreibung |
|---|---|---|
VL_SESSION_SECRET |
— | Pflicht. Zufälliger String ≥ 32 Zeichen |
VL_PASSWORD |
— | Initiales Login-Passwort |
VL_USERNAME |
admin |
Initialer Benutzername |
DATA_DIR |
/data |
Datenbank und Shot-Dateien |
PORT |
3000 |
Listening-Port |
CERT_PATH |
/certs/fullchain.pem |
TLS-Zertifikat (HTTPS aktiv, wenn vorhanden) |
KEY_PATH |
/certs/privkey.pem |
Privater TLS-Schlüssel |
Es gibt drei Wege, Shots in Visualizer Lite zu importieren:
Sobald das Upload to Visualizer-Plugin installiert und konfiguriert ist, werden Shots nach jeder Extraktion automatisch hochgeladen — kein weiterer Aufwand erforderlich.
Erfordert das Advanced REST API-Plugin auf dem Tablet.
Einstellungen → DE1-Import in Visualizer Lite öffnen, einen Datumsbereich festlegen und den Import starten. Die App streamt die Ergebnisse live und speichert das Datum des letzten erfolgreichen Imports — beim nächsten Öffnen der Einstellungen ist das Startdatum bereits vorausgefüllt, sodass ein Aufholimport mit einem Klick gestartet werden kann.
Visualizer Lite parallel zum offiziellen Visualizer nutzen: Wer den Direktimport statt des automatischen Uploads verwendet, kann das Upload to Visualizer-Plugin auf dem Tablet weiterhin so konfigurieren, dass Shots wie gewohnt an visualizer.coffee gesendet werden — beide sind vollständig unabhängig voneinander. So lassen sich beide Dienste parallel betreiben: die Community-Funktionen von visualizer.coffee und die lokale Historie in Visualizer Lite.
Geeignet für: initialen Massenimport einer bestehenden Shot-Historie, Nachholen von Shots nach einer Auszeit des Upload-Plugins, oder wenn der offizielle Visualizer das primäre Upload-Ziel bleiben soll.
Über die Upload-Schaltfläche in der oberen Navigationsleiste können eine oder mehrere .shot-Dateien vom eigenen Computer ausgewählt werden. Eine SHA-256-Duplikatserkennung verhindert, dass derselbe Shot mehrfach gespeichert wird — unabhängig vom Importweg.
Zwei Plugins arbeiten zusammen, um die DE1-Maschine mit Visualizer Lite zu verbinden:
Dieses Plugin übernimmt den automatischen Push-Upload: Nach jeder Extraktion sendet die DE1-App die .shot-Datei im Hintergrund an Visualizer Lite.
Es basiert auf dem Original-Plugin Upload to Visualizer von Johanna Schander, das ursprünglich für den Upload zu visualizer.coffee konzipiert wurde. Die hier enthaltene Version wurde erweitert, um benutzerdefinierte Upload-Ziele zu unterstützen — damit kann es statt des öffentlichen Dienstes auf die eigene Visualizer-Lite-Instanz zeigen. Es unterstützt sowohl HTTPS (empfohlen bei Zugriff von außerhalb des Netzwerks) als auch HTTP (unverschlüsselt — nur für vertrauenswürdige lokale Netzwerke geeignet, da Zugangsdaten im Klartext übertragen werden).
Installation: Den Ordner de1app/de1plus/plugins/visualizer_upload/ nach /de1plus/plugins/visualizer_upload/ auf das DE1-Tablet kopieren und die DE1-App neu starten. In den Plugin-Einstellungen die URL und Zugangsdaten der Visualizer-Lite-Instanz eintragen.
| Einstellung | HTTP (lokales Netzwerk) | HTTPS (externer Zugriff) |
|---|---|---|
| Visualizer URL | http://192.168.1.100:3000 |
https://meine-domain.de:3443 |
| Protokoll | Kein Zertifikat erforderlich | Gültiges TLS-Zertifikat notwendig |
| Empfehlung | Nur Heimnetzwerk | Internet-erreichbare Installation |
http://mit interner IP-Adresse verwenden für einfachen lokalen Zugriff ohne Zertifikate.https://mit Domain-Namen verwenden, wenn die Instanz aus dem Internet erreichbar ist.- Das Plugin zeigt eine Warnung, wenn eine HTTP-URL für eine internetfähige Instanz konfiguriert wird.
Die Advanced REST API-Extension ist ein separates Community-Plugin, das eine REST-Schnittstelle auf der DE1-Maschine bereitstellt. Visualizer Lite nutzt diese Schnittstelle für den Pull-basierten Import: Shots können direkt über die Einstellungsseite von Visualizer Lite importiert werden, ohne manuellen Dateitransfer.
Installation: Den Anweisungen im Advanced REST API Repository folgen. Dieses Plugin wird nur für den Direktimport benötigt.
- Decent Espresso — offizielle Website
- Decent Espresso auf GitHub
- Decent Diaspora auf Basecamp (Registrierung erforderlich)
- Visualizer — der offizielle Community Shot Visualizer
- Visualizer auf GitHub
- Upload to Visualizer Plugin — Original-Plugin von Johanna Schander (Basis der hier enthaltenen modifizierten Version)
- Advanced REST API Plugin — erforderlich für den Direktimport
| Ebene | Technologie |
|---|---|
| Runtime | Node.js 22 |
| Backend | TypeScript · Fastify 5 · Prisma 6 (SQLite + FTS5) |
| Frontend | TypeScript · React 19 · Vite 6 · TanStack Query 5 · react-router-dom 7 · i18next |
| KI | Anthropic SDK (claude-*) · OpenAI SDK |
| DE1-Plugin | Tcl (Decent App Scripting) |
| Infra | Docker (Multi-Stage, linux/amd64 + linux/arm64) · GitHub Actions |
| Tests | Vitest 4 · Testing Library |
KI-Analyse im Detail: docs/ai-analysis.de.md beschreibt Prompt-Design, das gestufte Kontext-System, Caching-Strategie und Kostenverfolgung.
Visualizer Lite läuft als einzelner Docker-Container. Die DE1-Maschine kommuniziert in beide Richtungen mit ihm; der Browser greift über Port 3000 auf denselben Container zu. Für die KI-Analyse ruft der Container bei Bedarf externe APIs auf (Claude oder OpenAI) — für alle anderen Funktionen ist kein API-Key erforderlich.
graph LR
DE1["🖥️ DE1 Espresso-Maschine\n(Decent App + Plugin)"]
VL["📦 Visualizer Lite\n(Docker Container :3000)"]
Browser["🌐 Browser"]
AI["☁️ KI-API\n(Claude / OpenAI)"]
DE1 -- "Push: POST /api/shots/upload\n(nach jeder Extraktion)" --> VL
VL -- "Pull: Advanced REST API\n(manuell über Einstellungen)" --> DE1
Browser -- "HTTP / HTTPS" --> VL
VL -- "Shot-Analyse\n(auf Abruf, optional)" --> AI
Der Container startet einen einzelnen Node.js-Prozess. Fastify liefert sowohl die REST API als auch das vorcompilierte React-SPA über denselben Port aus. Alle Daten liegen auf dem gemounteten /data-Volume — keine externen Dienste notwendig.
graph TB
subgraph Container["Docker Container (Node.js 22)"]
direction TB
subgraph Fastify["Fastify"]
Auth["Auth\n/api/auth"]
ShotAPI["Shots\n/api/shots"]
Upload["Upload\n/api/shots/upload"]
DE1API["DE1 Import\n/api/de1"]
Stats["Stats & Export\n/api/stats /api/export"]
Analysis["Analyse\n/api/analysis"]
Static["Static\n(React SPA)"]
end
Prisma["Prisma ORM"]
FTS["SQLite FTS5\n(Volltextsuche)"]
end
subgraph Volume["/data (Volume-Mount)"]
DB[("visualizer.db\n(SQLite)")]
Files[("files/\n*.shot (Rohdaten)")]
end
AI["☁️ KI-API\n(Claude / OpenAI)"]
ShotAPI & Upload & DE1API & Stats --> Prisma
Analysis --> Prisma
Analysis -- "auf Abruf" --> AI
Prisma --> DB
DB --> FTS
Upload & DE1API --> Files
Auth --> Prisma
Es existieren zwei unabhängige Importwege — Push von der Maschine und Pull auf Abruf:
sequenceDiagram
participant DE1 as DE1-Maschine
participant Plugin as DE1-Plugin
participant API as Visualizer API
participant DB as SQLite
rect rgb(30, 50, 40)
note over Plugin,DB: Push (automatisch nach jedem Shot)
Plugin->>API: POST /api/shots/upload\n(multipart .shot-Datei)
API->>API: Parsen + SHA-256-Deduplizierung
API->>DB: INSERT oder UPDATE Shot
API-->>Plugin: 200 OK / 409 Duplikat
end
rect rgb(30, 40, 55)
note over DE1,DB: Pull (manuell über Einstellungsseite)
API->>DE1: GET /api/v2/shots (Advanced REST API)
DE1-->>API: Liste der Shot-Dateinamen
API->>API: Filterung nach Datumsbereich
loop Jede Shot-Datei
API->>DE1: GET /api/v2/shots/{dateiname}
DE1-->>API: .shot-Dateiinhalt
API->>API: Parsen + SHA-256-Deduplizierung
API->>DB: INSERT oder UPDATE
API-->>API: NDJSON-Fortschritt streamen
end
end
visualizer-lite/
├── packages/
│ ├── api/ # Fastify Backend (Node.js)
│ │ ├── src/
│ │ │ ├── routes/ # auth, shots, upload, de1, stats, export, search, analysis
│ │ │ ├── services/ # shotService, searchService, de1Service, analysisService, …
│ │ │ ├── parsers/ # decent.ts — .shot-Datei-Parser
│ │ │ └── plugins/ # auth (JWT + Cookie)
│ │ └── prisma/
│ │ └── schema.prisma # SQLite-Schema (Shot, ShotAnalysis, Tag, Settings)
│ └── web/ # React 19 + Vite 6 Frontend
│ └── src/
│ ├── pages/ # ShotList, ShotDetail, ShotEdit, Stats, …
│ ├── components/ # ShotCard, Pagination, SearchBar, …
│ └── api/client.ts # Typsicherer Fetch-Wrapper
├── de1app/ # DE1 Tcl-Plugin (Push-Upload)
└── Dockerfile # Multi-Stage: builder → runtime
erDiagram
Shot {
string id PK
string sha256 UK
string filePath
datetime startTime
float duration
float beanWeight
float drinkWeight
string beverageType
string profileTitle
string beanBrand
string beanType
string grinderModel
int espressoEnjoyment
string shotData "JSON (Zeitreihen)"
}
ShotAnalysis {
string id PK
string shotId FK
string analysisType "detail | stats"
string aiModel
string barista "JSON-Array"
string roaster "JSON-Array"
int tokenInputCount
int tokenOutputCount
float costInputUsd "nullable"
float costOutputUsd "nullable"
datetime createdAt
}
Tag {
string id PK
string name UK
}
Settings {
string key PK
string value
}
Shot ||--o| ShotAnalysis : "Analyse"
Shot }o--o{ Tag : "Tags"
# Installation
npm install
cd packages/api && npx prisma migrate dev
# Terminal 1 — API (Port 3000)
cd packages/api
VL_SESSION_SECRET="dev-secret-must-be-at-least-32-chars!" \
VL_PASSWORD=test \
npm run dev
# Terminal 2 — Web (Port 5173)
cd packages/web && npm run dev
# Tests
cd packages/api && npx vitest runNur nötig, wenn das Image selbst gebaut werden soll (z.B. für lokale Entwicklung oder einen Fork).
Auf dem Entwickler-Rechner:
# Für lokale Nutzung (natives Platform)
docker build -t visualizer-lite:local .
# Für Synology NAS oder andere x86_64/amd64-Geräte (Cross-Compile von Apple Silicon)
docker build --platform linux/amd64 -t visualizer-lite:nas .
docker save visualizer-lite:nas | gzip > visualizer-lite.tar.gz# Übertragen
scp visualizer-lite.tar.gz admin@<NAS-IP>:/volume1/docker/
# Auf dem NAS (via SSH)
docker load < /volume1/docker/visualizer-lite.tar.gz
mkdir -p /volume1/docker/visualizer-lite/data/files
chown -R 1000:1000 /volume1/docker/visualizer-lite/datadocker run -d \
--name visualizer-lite \
--restart unless-stopped \
-p 3000:3000 \
-v /volume1/docker/visualizer-lite/data:/data \
-e VL_SESSION_SECRET="$(openssl rand -base64 48)" \
-e VL_PASSWORD="dein-passwort" \
visualizer-lite:nasVisualizer Lite ist ein persönliches Hobbyprojekt, das unter der Business Source License 1.1 kostenlos für den persönlichen, nicht-kommerziellen und internen Geschäftsgebrauch (einschließlich Self-Hosting) zur Verfügung gestellt wird. Die Nutzung dieser Software erfolgt vollständig auf eigenes Risiko.
Die Software wird „wie besehen“ ohne jegliche Gewährleistung bereitgestellt. Der Autor bietet keinen Support und übernimmt keine Haftung für Schäden jeglicher Art, die aus der Nutzung, der Nichtnutzbarkeit oder dem Missbrauch dieser Software entstehen — einschließlich, aber nicht beschränkt auf Datenverlust, Sicherheitsvorfälle oder Systemausfälle. Die vollständigen Haftungsausschlüsse und Lizenzbedingungen sind in LICENSE.md festgelegt.
Jeder Nutzer ist allein verantwortlich für die Sicherheit und Integrität seiner eigenen Systeme sowie für sämtliche Konfigurationsentscheidungen — einschließlich Netzwerkzugang, Authentifizierungskonfiguration und TLS-Einrichtung —, insbesondere wenn die Instanz außerhalb eines vertrauenswürdigen lokalen Netzwerks erreichbar ist.










