- Python 50%
- HTML 22.4%
- JavaScript 13.3%
- CSS 12.7%
- Lua 1.6%
| app | ||
| koreader-plugin | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| apache-koquotes.conf.example | ||
| babel.cfg | ||
| CHANGELOG.md | ||
| COPYRIGHT | ||
| docker-compose.yml | ||
| Dockerfile | ||
| HAUPTKASTEN-GUIDE.md | ||
| LICENSE | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
KOReader Quote Library
Self-hosted server that receives highlights from KOReader and presents them as a browsable, searchable quote library — growing into a personal knowledge workspace (social reading + Zettelkasten + an in-browser book reader).
Multi-user for family & friends: each person has their own login and API token, and only ever sees their own highlights (sharing is explicit, per node). Accounts are created by the admin — there is no public signup.
Two pieces:
- This server — FastAPI + SQLite + a server-rendered web frontend (Jinja2, no JS framework; JS only as progressive enhancement).
- A KOReader exporter target (
quotes.lua) that POSTs highlights here and talks to the server for plugin updates and dictionary downloads. Canonical source + install instructions inkoreader-plugin/.
Wohin das hier soll (Big Picture)
Das Projekt entwickelt sich vom reinen KOReader-Zitatarchiv zu einem Wissens-Arbeitsplatz — einem Ort, an dem mit Zitaten, Zetteln, Büchern und ihren Verknüpfungen an Ideen gearbeitet wird. Kurz: ein Gedächtnis und eine Brutkammer.
Es geht darum, Informationen und Wissen — textlich, nah an konventionellen Publikationsmechanismen — aus unterschiedlichen Quellen zu sammeln, zu organisieren und zu durchdenken: KOReader-Highlights, von Hand angelegte Bücher und Zitate, im eingebetteten Web-Viewer markierte Stellen und (geplant) Zitate aus Web-Artikeln. So wird das Webinterface vom Archiv zum vollwertigen Lese-, Exzerpier- und Denkwerkzeug.
Der ausführliche, gestufte Plan steht in ROADMAP.md; die
nutzersichtbaren Änderungen je Version in CHANGELOG.md.
Funktionsumfang (Stand v1.97.0, 2026-08-12)
Reader ↔ Server
- Highlight-Export vom Gerät per Plugin-Target; idempotent (erneuter Export aktualisiert statt dupliziert), ein Request pro Buch, Fehler werden pro Buch gemeldet.
- Lesestatistiken pro Gerät: Lesedauer, gelesene Seiten, Position im Buch
(
percent_finishedaus dem Sidecar), erstmals/zuletzt gelesen, Seitenzahl. Mehrere Geräte werden pro Buch zusammengeführt (Geräte-Tabelle + Aggregat). - OTA-Plugin-Update: „Check for plugin update" im Gerätemenü zeigt installierte vs. angebotene Version und installiert erst nach Bestätigung.
- Wörterbuch-Verteilung: Stardict-Wörterbücher liegen auf dem Server und
lassen sich vom Gerät aus herunterladen — komponentenweise gestreamt, ohne
On-Device-Unzip, nach
data/dict/<slug>/; ohne Neustart nutzbar. Mit Fortschrittsanzeige (Bytes + Prozent) und ohne Gesamtdeadline, damit auch 200-MB-Parts über langsames Reader-WLAN durchlaufen.
Bibliothek (Web)
- Bücherliste (sortierbar nach zuletzt/Titel/Autor), Autorenansicht.
- Buchseite: Zitate mit Markierungsfarbe und -stil, Kapitel, Seite, Lesestatistiken, Geräteherkunft.
- Pagination für lange Listen und für die Zitate innerhalb eines Buchs: JS-frei (reine Links), 25/50/100/Alle pro Seite, die Wahl wird per Cookie seitenübergreifend gemerkt; der Pager erscheint erst ab >25 Einträgen.
- Titel/Autor-Korrektur als Overlay (Original bleibt erhalten, Merge-Key unangetastet).
- Buch-Zusammenführung für Dubletten (Alias + Snapshot-Undo).
- Zitate bearbeiten (Overlay + Diff-Ansicht gegen das Original) und ausblenden/wiederherstellen — einzeln oder im Auswahlmodus gesammelt. Im Bearbeiten-Feld gesetzte Zeilenumbrüche bleiben erhalten (z. B. Gedichte in Versform).
- Stellen im Zitat hervorheben („✍ markieren"): einzelne Textstellen mit der Maus auswählen und wie mit einem Textmarker einfärben (Farbe wählbar); der Zitattext bleibt unverändert (Overlay aus Zeichen-Offsets).
- Bemerkungen pro Buch und Annotationen/Kommentare an Zitaten (Markdown). Lange Felder (Zettel-Text, Buch-Bemerkungen, Sammlungs-Beschreibung) haben einen WYSIWYG-Editor (selbst gehostetes Toast UI, keine externen Aufrufe) mit Bild-Upload (Bilder liegen privat auf dem eigenen Server). Kurze Felder (Kommentare, Annotationen) behalten die schlanke Markdown-Toolbar + Vorschau.
- Zitat-Komfort: Schriftgröße per Knopf (Normal/Groß/Sehr groß), „🔗 Link" kopiert einen Permalink zum Zitat, „↪ Zum Zitat" springt aus Suche/Tags zur Stelle in der Buchansicht.
- Manuell anlegen: Bücher von Hand erstellen und Zitate selbst eintippen (z. B. aus einem Papierbuch, mit Seitenangabe) — unabhängig vom Gerät.
- Echtes Löschen für selbst erzeugte Zitate (Provenienz „Manuell"/„Web"); Geräte-Zitate bleiben nur ausblendbar (sie kämen beim Sync zurück).
Arbeitsplatz / Im Buch lesen
- Eingebetteter Buch-Viewer (epub.js): das Buch wird live aus der eigenen calibre-web-Instanz durchgestreamt — kein Doppel-Hosting. Einmalige Verknüpfung pro Buch über die OPDS-Suche.
- Zitat im Buch öffnen: springt an die Fundstelle und markiert die ganze Stelle (knotenübergreifender Matcher, auch über Kursiv-/Absatzgrenzen).
- Standalone lesen: beliebiges EPUB aus calibre-web öffnen, auch ohne vorhandene Zitate; ein Bucheintrag entsteht erst beim ersten Markieren.
- Im Buch markieren → Zitat: Textstelle auswählen → wird als Zitat (Provenienz „Web", mit exakter CFI und Kapitel aus dem Inhaltsverzeichnis) gespeichert; öffnet sich später punktgenau.
- Split View: links der Viewer, rechts die echte Buchseite mit allen Zitaten; markieren erscheint sofort, Zitat anklicken springt im Buch; Spalte per Ziehleiste anpassbar.
- Wörterbuch & Wikipedia im Buch: einzelnes Wort auswählen → Popup mit
Definition (durchblätterbar); jedes Wort im Eintrag ist antippbar (auch
Verweise wie „↑ suspect", die im Wörterbuch nur Text sind — wie am E-Reader)
- Wikipedia-Schnelllinks in frei wählbaren Sprachen (auf der Konto-Seite,
z. B.
de, en, fr). Pro Buch wählbar, welche Wörterbücher genutzt werden (⚙ in der Leseleiste, am Buch gespeichert).
- Wikipedia-Schnelllinks in frei wählbaren Sprachen (auf der Konto-Seite,
z. B.
- Navigation: Inhaltsverzeichnis (☰) zum Kapitelsprung; Fortschritts-Schieber mit %-Anzeige zum Springen an eine Stelle. Web-Zitate erfassen die Leseposition (%) statt einer Seite.
- Komfort im Viewer: Schrift-Zoom, Volltextsuche im Buch.
Web-Artikel
- Dossiers: Mappen auf Dauer, die viele Artikel zu einem Unterthema unter einem Kopf bündeln. Wer über Monate 25 Belege zum selben Thema sammelt, hat in der Liste eine Zeile, die sich an Ort und Stelle aufklappt, statt 25. Ein Dossier sortiert nicht um: die Artikel behalten ihren Typ, bleiben unter „Artikel" auffindbar und können in mehreren Mappen zugleich liegen (immer nur ein Verweis, nie eine Kopie). Ein Dossier lässt sich verschlagworten, an Zettel verknüpfen und in Sammlungen legen — am Zettel hängt dann ein einziger Eintrag statt 25.
- Kompakte Liste mit Werkzeugzeile: jede Quelle als dichte Zeile (Titel bis zwei Zeilen, Domain statt langer URL, volle Adresse im Tooltip); ein Tipp-Filter grenzt die Liste beim Schreiben sofort ein und sucht dabei in zugeklappte Mappen hinein; die Mehrfachauswahl erscheint erst über „☐ Auswählen" (ohne JavaScript ist alles dauerhaft sichtbar).
- Nach Herkunft eingrenzen: „Aus Feeds" (mit Zähler) oder „Selbst erfasst". Der Feed-Blick listet alle übernommenen Artikel flach — auch die in einer Mappe —, damit sich ein Schwung Importe in einem Rutsch auswählen und löschen lässt. (Flach deshalb, weil ein Dossier keine Herkunft hat und eine Mappe, die nur die Hälfte ihrer Artikel zeigt, über ihren Inhalt lügen würde.)
- Zitate aus Websites als eigene Quelle, gleichberechtigt mit Buchzitaten (verknüpfbar, taggbar, kommentierbar). Eigener „Artikel"-Bereich; Zitate desselben Links werden zu einem Artikel gruppiert, mit „Quelle öffnen".
- Erfassen per Formular oder Bookmarklet (Konto-Seite): Text markieren → Lesezeichen klicken → Formular ist vorausgefüllt. Artikel lassen sich auch ohne Zitat speichern (nur die Quelle sichern). (Geplant: generischer API-Endpoint für fremde Browser-Erweiterungen.)
- kuotes-App (PWA) fürs Handy: installierbar, mit „Senden an kuotes" im Teilen-Menü (Text markieren → Teilen → vorausgefülltes Formular). Zeigt die gesammelten Web-Zitate (durchsehen, bearbeiten, löschen) und hält bei fehlender Verbindung eine editierbare Warteschlange vor, die automatisch überträgt, sobald der Server erreichbar ist. (Das Teilen-Ziel selbst gibt es nur in Chromium-Browsern; Firefox/Fennec und iOS/Safari unterstützen die Web-Share-Target-API nicht — dort das Zitat in der App von Hand eintragen.)
- Volltext-Archiv: zu einem Artikel lässt sich der ganze Seitentext serverseitig per trafilatura nachladen (Titel, Autor, Datum, Fließtext), versioniert mit Diff; Stellen markieren/hervorheben wie bei Zitaten.
- Als EPUB: jeder Artikel mit Volltext lässt sich als EPUB laden — gebaut direkt aus dem gespeicherten Markdown (keine externe Dienste), inkl. rückverlinkter Fußnoten.
- Eigener OPDS-Katalog: die Artikel stehen als OPDS-Katalog bereit, den KOReader auf Kindle und PocketBook abonniert und als EPUB aufs Gerät lädt. Zugang per kurzem, E-Ink-tippbarem OPDS-Token (Konto, getrennt vom Login).
- Reader (geordnete Lesemappen): mehrere Artikel in selbst gewählter Reihenfolge zu einem mehrkapiteligen EPUB bündeln (Titelseite, Inhalts verzeichnis mit Autor + Datum, ein Kapitel je Artikel), ebenfalls über OPDS ausgeliefert. Artikel über durchsuchbare Liste mit Autovervollständigung und Autor-Filter hinzufügen. Markierungen aus dem EPUB (Einzelartikel oder Reader) finden in KOReader wieder zum jeweiligen Quellartikel zurück.
- Bilder: gefundene Artikelbilder werden beim Erfassen/Neu-Holen lokal gespeichert (nichts lädt beim Lesen mehr von fremden Servern) und auch ins EPUB eingebettet. Beim Erfassen zeigt ein Vorschau-Raster jedes Bild mit einer Checkbox zum Ab-/Auswählen; selbst in den Volltext eingefügte Bilder (z. B. aus dem Lesemodus kopiert) bleiben ebenfalls erhalten. Einzelne Bilder lassen sich später redaktionell entfernen.
- Deckblätter: jedes Artikel- und Reader-EPUB bekommt ein schlichtes, automatisch erzeugtes Cover (Titel/Autor/Datum) für hübschere OPDS-Vorschauen; alternativ ein Artikelbild als Cover wählen oder für einen Reader ein eigenes Deckblatt hochladen.
- Deckblatt-Editor (Artikel und Reader): das EPUB-Cover frei gestalten —
Textfelder direkt auf der Fläche verschieben, drehen und skalieren (mehrere
pro Cover), je mit Fett/Kursiv/Großbuchstaben/Schatten/Laufweite/Ausrichtung/Farbe
und optionaler Schriftart (eigene TTFs in
app/assets/cover_fonts/); Bild als Hintergrund eingepasst, füllend oder frei (zoom-/verschiebbar). Serverseitige „echte" Vorschau; ohne JavaScript bleibt ein einfaches Formular als Rückfallebene. - RSS/Atom-Feeds: Artikel aus Feed-Aggregatoren (FreshRSS Starred, Linkding
Bookmarks u. a.) importieren — der Feed wird geladen, die Links als Artikel durch
dieselbe Pipeline gezogen (mit „x von y"-Fortschrittsanzeige) und optional gleich
in einen Reader gelegt. Feeds lassen sich auch abonnieren: ein Hintergrund-Poller
holt neue Einträge automatisch, einsortiert nach Ziel (nur Artikelliste / fester Reader /
rollender Monats-Reader). Feed-Artikel sind als Herkunft
feedmarkiert, werden nach einer einstellbaren Frist automatisch aufgeräumt (mit „★ behalten") und lassen sich gesammelt neu holen (Reader-Refresh, inkl. Bilder).
Zettelkasten
- Eigene Zettel (freistehende Notizen, Markdown).
- Schlagwörter an Zitaten, Zetteln und Büchern; Schlagwort-Index als Registratur, Tag-Seiten.
- Verknüpfungen zwischen beliebigen Knoten (Zitat/Zettel/Buch); die
Verbindungen eines Eintrags lassen sich über „⤢ alle auf einer Seite"
(
/connections) gebündelt ansehen. - Sammlungen: geordnete Folgezettel-Sequenzen, umsortierbar — mit Notiz pro Eintrag auf der Übersicht („warum ist das hier"). Jeder Eintrag zeigt zudem read-only seinen eigenen Kontext (Zitate ihre Schlagwörter + Annotationen, Zettel ihre Beschreibung).
- Sammelkorb: Zitate verstreut unterwegs einsammeln und am Ende in einem Rutsch auf einen Zettel kippen, statt 60 Zitate einzeln zu verknüpfen. Knopf an jeder eigenen Zitatkarte, Sammelaktion in der Mehrfachauswahl, Zähler in der Leiste; auf der Korbseite entweder an einen neuen Zettel oder an einen bestehenden. Gedacht als Transport für eine Arbeitssitzung — das Dossier ist die Mappe auf Dauer.
- Autosave im Editor: lange Texte werden während des Schreibens zwischengespeichert, damit ein Absturz oder ein versehentlich geschlossener Tab die Arbeit nicht mitnimmt.
Songtexte
- Eine Zeile aus einem Lied zitieren und den ganzen Liedtext als Kontext dazulegen; Klick aufs Zitat zeigt die Zeile im Volltext hervorgehoben (wie „im Buch öffnen", nur ohne Buch dahinter). Liedtext bewahrt seine Zeilenumbrüche.
- In einem Schritt erfasst (Zeile + Liedtext + Interpret/Titel/Album/Jahr); das Zitat ist danach wie ein Buchzitat durchsuchbar, verschlagwortbar, teilbar und in den Hauptkasten verknüpfbar. Eigenes Lyrics-Bookmarklet. Der Interpret ist anklickbar (andere Lieder desselben Künstlers).
Hauptkasten (zweiter Zettelkasten — eigene Gedanken)
Ein eigenständiges zweites System neben dem „Quellenkasten" (allem oben), nach
Vorbild von Luhmanns geteiltem Zettelkasten: hier stehen eigene, in eigenen
Worten formulierte Gedanken statt fremder Zitate. Privat by design — nicht
teilbar (/hauptkasten).
- Gedanke als eigene Karte (Titel optional, Markdown-Body, Toast-Editor); voll anlegen / lesen / bearbeiten / löschen.
- Löschen, dem man trauen kann: Gelöschtes landet im Papierkorb und lässt sich zurückholen; nach dem Löschen landet man beim Elternzettel statt im Nirgendwo. Mehrere Gedanken lassen sich in allen Ansichten zugleich auswählen, ganze Stränge auf einmal entfernen. Und beim Abzweigen entstehen keine Dubletten mehr — ein Archiv, das still etwas verschluckt oder verdoppelt, ist eines, dem man aufhört zu vertrauen.
- Folgezettel-Baum: einen Gedanken fortsetzen (Kind) oder abzweigen (Geschwister); organisch vergebene Adressen (1, 1a, 1a1 …), ausklappbare Bäume und eine „Strang lesen"-Ansicht (ein Strang am Stück).
- Baumansichten: am Gedanken/Strang ein „Umgebung im Baum" (Aufwärts-/ Kontextbaum: Vorfahren-Spine offen, Nachbarn eingeklappt) sowie ein grafischer „Stammbaum" je Strang — ein server-gerendertes Diagramm von links nach rechts (Wurzel links, Folgezettel nach rechts), umschaltbar zwischen kompakt und vollem Text, komplett ohne JavaScript navigierbar.
- Arbeitsfläche: Stränge als frei verschiebbare Karten auf einem zoom- und scrollbaren Tisch (mehrere Stränge nebeneinander, Geschwister als 2D-Baum). Zwei Modi: Frei (bewusst flüchtig, nichts wird gespeichert) und Sortieren — hier zieht eine Karte ihren ganzen Strang mit, Ablegen auf eine andere merkt sie als Folgezettel vor, und „Speichern" schreibt alle Vormerkungen auf einmal in den Kasten zurück (der Ketten-Knopf ⛓ bzw. Alt+Ziehen hängt nur die Einzelkarte um; Adressen pro Karte wählbar).
- Lesemodus: ein ganzer Strang als durchgehender Fließtext — die Zettel verschmelzen ohne Karten/Köpfe zu einem zusammenhängenden Lesetext. Zettelgrenzen erscheinen nur beim Überfahren (Referenz + Rahmen) oder dauerhaft per Schalter „Zettelgrenzen zeigen" (ohne JavaScript). Zeigt immer den kompletten Strang ab der Wurzel, egal von welchem Zettel aus geöffnet, und springt zur Ausgangsstelle.
- Nachträglich einsortieren: einen lose erfassten (wurzellosen) Gedanken
später zum Folgezettel unter einem passenden Zettel machen — er bekommt
dort eine frische Folgeadresse. Das Ziel findet man per Stichwort oder direkt
über die Adresse (z. B.
1a,2). Es wandern nicht nur einzelne Blätter, sondern ganze Stränge — beim Umhängen wählbar zwischen neu nummerieren (nahtlos in den Ziel-Strang) und Adressen behalten (sichtbar „von woanders"), zyklensicher. - Hauptkasten-App (PWA) zum Erfassen unterwegs: installierbar, offline (lokal gequeued, lädt bei Netz automatisch hoch), Token-Auth; vor dem Schreiben erst ins Register schauen und gleich als Folgezettel anhängen. Ein „Letzte Gedanken"-Chip listet die zuletzt verfassten Gedanken zum direkten Anknüpfen (Folgezettel/Strang) ohne Suche. Einrichtung über die Konto-Seite (auf dem Server: HTTPS nötig).
- Brücke Highlight → Gedanke: ein Zitat vormerken (opt-in Inbox), dann in eigenen Worten weiterdenken; die Quelle steht als gedämpfte Referenz dabei (kein Copy-Button), beim Speichern automatisch verknüpft + als verarbeitet markiert.
- Register: pro Schlagwort 1–2 Gedanken als sparsame Einstiegspunkte (★), Limit konfigurierbar.
- Verbindungen mit Begründung (warum sind zwei Gedanken verknüpft) und Kommunikationspartner-Hilfen: 🎲 Zufallsgedanke, „hängt noch frei", „vielleicht verwandt?".
- Schreibtisch als Startseite (
/): beide Kästen nebeneinander; die Bücherliste liegt unter/books. - Export / Langlebigkeit: der ganze Kasten als self-contained Markdown-ZIP
(
/hauptkasten/export) — Quellen eingebettet, ohne diese Software „neu auspackbar". Im selben Archiv liegtkasten.html: derselbe Kasten als eine statische Seite mit den Strängen als gezeichneten Bäumen, deren Knoten zum jeweiligen Zettel springen. Kein Server, kein Netz, kein JavaScript — die Markdown-Dateien sind die verlustfreie Fassung, die HTML-Seite die lesbare.
Suche
- Läuft über einen Volltextindex (SQLite-FTS5, Trigram) statt über einen Tabellendurchlauf — bei selektiven Begriffen rund zehnmal schneller, und das bleibt so, wenn die Bibliothek wächst. An der Semantik ändert sich nichts: Teilwörter finden Komposita weiter („kasten" → „Zettelkasten").
- Volltextsuche über Zitate, Titel, Autoren; zuschaltbare Schlagwort-Filter-Chips; Typeahead mit gruppierten Vorschlägen; buchgruppierte Trefferliste.
- Mehrwort-Suchen: jedes Wort muss vorkommen, egal wo und in welcher Reihenfolge — auch über Feldgrenzen („tolstoi krieg" findet das Zitat im Tolstoi-Buch). Groß/Klein wird auch bei Umlauten gefaltet („über" findet „Über…", „strasse" findet „Straße").
- Mehrfachautoren einzeln: gemeinsam verfasste Bücher (gespeichert als „A & B") werden als einzelne Autoren behandelt — die Vorschläge zeigen den getippten Namen ohne Co-Autor, und die Treffer-Zeile zählt distinct Einzelautoren; jeder Name ist eigenständig anklickbar.
Teilen & Diskussion
- Teilen von Zitaten, Zetteln und Büchern an andere Accounts — lesend oder mitdiskutierend.
- Threaded Kommentare mit verschachtelten Antworten, farbigen Tiefen-Rails, Bearbeitungszähler („2× bearbeitet") und Tombstone-Löschung (gelöschte Kommentare bleiben als „Kommentar gelöscht" im Thread sichtbar).
- Benachrichtigungen: Posteingang ungelesener Ereignisse (geteilt, kommentiert, geantwortet) mit relativer Zeitangabe und Badge in der Navigation.
Export
- Einzelnes Buch/Artikel oder die ganze Bibliothek als Markdown oder Text, inklusive eigener Annotationen.
Wörterbücher (Web)
- Admin-Upload von Stardict-Wörterbüchern — als ZIP oder Einzeldateien, mit Live-Checkliste der Pflichtteile, Fortschrittsbalken und Abbrechen.
- Download-Seite für alle Nutzer (ZIP je Wörterbuch).
- Nachschlagen im Browser: Suchbox auf
/dictionariesmit Typeahead, über alle Wörterbücher oder eines gezielt; mehrteilige Wörterbücher (z. B.…_P1..P4) erscheinen als EIN Eintrag und werden zusammen durchsucht. Server liest Stardict direkt (Dictzip-Random-Access, SQLite-Wortindex je Wörterbuch, beim Start/Upload vorgebaut); Einträge laufen durch einen Allowlist-HTML-Sanitizer, XDXF wird konvertiert, Null-Vote-Zähler („(+0, -0)") werden gefiltert. Funktioniert ohne JS. - Im Buch nachschlagen: im Viewer ein Wort auswählen → Popup mit Definition (siehe Arbeitsplatz). Querverweise (↑ anderes Stichwort) sind klickbar.
Administration & Betrieb
- Anmeldung im Browser über eine Session-Cookie-Login-Seite (mit Rate-Limiting und Logout) statt HTTP-Basic; das API-Token bleibt für Geräte.
- Accounts anlegen/löschen, Token rotieren, Passwort setzen — im Web-Admin und per CLI. Konto- und Admin-Seite nach Themen gegliedert (Anker-Navigation, Token-Kopierknopf, User-Suche im Admin).
- Konfigurierbarer Seitentitel (Header/Footer/Browser-Tab) + eigenes Favicon.
- Setup-Bundle: vorkonfigurierte
quotes_exporter.lua(Token schon eingetragen) + Plugin + Anleitung als ZIP von der Profilseite. - Versions-Footer auf jeder Seite +
/api/healthfür Deploy-Sichtbarkeit. - Tägliches Datenbank-Backup (
VACUUM INTO data/backups/, 14 Tage Rotation, holt Verpasstes nach;QUOTES_BACKUP_DIR/QUOTES_BACKUP_KEEP). Hinweis:data/uploadsist separat zu sichern. - Sicherheits-Härtung: serverseitiges Nachladen geht ausschließlich über eine
zentrale Prüfung (nur http/https, keine internen Adressen, Neu-Prüfung bei
jeder Weiterleitung); Login/Sprachwahl akzeptieren nur lokale Rücksprungziele;
das Rate-Limit sieht die echte Client-IP hinter dem Proxy; Abmelden ist ein POST.
Für selbstgehostete Aggregatoren im LAN gibt es
QUOTES_ALLOW_PRIVATE_FETCH.
Darstellung
- Sechs Themes: Archiv (dunkel, Default) / Archiv (hell) — getippte
Karteikarten im Zettelschrank mit Akten-/Ordner-Layout, Zettelkasten,
Nacht, Bibliothek, Klar. (Ein siebtes, Absolutic nach dem
Absoluten Film von 1926, liegt fertig auf
origin/absolutic-theme, ist aber seit Juni 2026 unangetastet und nicht entschieden — nicht ausgeliefert.) - Schriftgröße umstellbar (Normal / Groß / Sehr groß) — skaliert das ganze Interface, gemerkt pro Browser.
- Zweisprachig (Deutsch / Englisch) — die ganze Oberfläche ist über
Babel + gettext lokalisiert (Jinja-
i18n-Extension; deutscher Quelltext =msgid,en-Katalog unterapp/translations/). Sprachwahl per Umschalter in der Fußzeile, sonst nachAccept-Languagemit Fallback Deutsch. Tragende Eigennamen (Hauptkasten, Quellenkasten, Zettel, Strang, Folgezettel, Register …) bleiben im Englischen bewusst deutsch.
Architecture
KOReader (per-user Bearer token) --HTTPS--> Apache2 (TLS, reverse proxy) --> FastAPI :8000 --> SQLite
|
browser <--- web UI (session-cookie login)
- Multi-user, admin-provisioned.
userstable; every book/highlight is scoped to auser_idand every read query filters by it. Sharing grants are explicit per node (sharestable). - Idempotent ingest. Books keyed by
(user_id, book_key), highlights by a stable uid hash — re-exporting updates instead of duplicating. - Overlay, not overwrite. Edits to quote text and book title/author are stored as overrides next to the original, so re-imports never clobber manual fixes and a diff view stays possible.
- Auth. Bearer token per user for the API (
/api/*); session-cookie login (username/password, rate-limited) for the web UI — no HTTP Basic. - Dictionaries on disk. Stardict bundles live as folders under
DICT_DIR(default: next to the DB; Docker:/data/dictionaries) — no DB table, the filesystem is the source of truth. - Privacy / self-contained. No third-party calls from the web UI: the rich
editor (Toast UI) and all JS/CSS are vendored under
app/static/(no CDN), and uploaded images are served only to logged-in users fromdata/uploads. Nothing about the user's library leaves the server.
Run locally (development)
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# create an account (prompts for a password, prints an API token)
python -m app.cli adduser alice --admin
uvicorn app.main:app --reload
Open http://127.0.0.1:8000 and log in as the user you created.
Test the ingest endpoint with the token printed by adduser:
curl -X POST http://127.0.0.1:8000/api/highlights \
-H "Authorization: Bearer <THE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"version": "quotes/1.0.0",
"documents": [{
"title": "Demo Book", "author": "A. Author", "md5sum": "demo123",
"number_of_pages": 100,
"highlights": [
{"text": "A first quote.", "page": 12, "chapter": "One", "color": "yellow", "time": 1700000000},
{"text": "A second quote.", "note": "my note", "page": 40, "time": 1700000100}
]
}]
}'
Account management (CLI)
python -m app.cli adduser <name> [--admin] # create account, print API token
python -m app.cli listusers # list accounts
python -m app.cli newtoken <name> # rotate a user's API token
Inside Docker prefix with docker compose exec quotes.
Deploy (Debian 12 + Apache2)
docker-compose.yml and the Dockerfile work as-is with Podman:
podman build -t koquotes .
podman-compose up -d
podman-compose exec quotes python -m app.cli adduser <name> --admin
Rootless Podman bind-mount note: if the container can't write ./data, add a
:Z (SELinux) / check ownership, or run podman unshare chown -R 0:0 ./data.
The service is published on 127.0.0.1:23623 (uvicorn listens on 8000 inside
the container; change the host port in docker-compose.yml if needed). Put
Apache in front for TLS — see
apache-koquotes.conf.example. The SQLite DB and the uploaded
dictionaries live in ./data; back both up.
Environment variables
| Variable | Purpose |
|---|---|
QUOTES_SESSION_SECRET |
Signs session cookies. Required in production (with QUOTES_SESSION_HTTPS=true the app refuses to start without it); set it in .env. Generate: openssl rand -hex 32. |
QUOTES_SESSION_HTTPS |
true behind a TLS reverse proxy → Secure cookie. |
QUOTES_ALLOW_PRIVATE_FETCH |
true lets server-side fetches (article capture, RSS feeds, article images) reach private/loopback/LAN addresses. Off by default (SSRF protection); enable only if your feed aggregator runs on localhost/LAN. |
QUOTES_PUBLIC_URL |
Public base URL behind the proxy; pre-fills the device config. |
QUOTES_TZ |
Timezone for displayed timestamps (stored UTC). Default Europe/Berlin. |
QUOTES_DB_PATH / DICT_DIR |
DB file and dictionary folder (Docker: under /data). |
QUOTES_BACKUP_DIR / QUOTES_BACKUP_KEEP |
Daily DB snapshots (VACUUM INTO). Default: backups/ next to the DB file, last 14 kept. Note: data/uploads (editor images) are plain files, not part of the DB snapshot — include that folder in your own backup. |
Server-side fetches are SSRF-guarded: the target host must resolve to a public
IP and every HTTP redirect hop is re-validated (see app/safe_fetch.py). The
security review and its remaining, by-design items are in SECURITY-FINDINGS.md.
API (device-facing)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/health |
none | liveness check + version |
| POST | /api/highlights |
Bearer | ingest highlights + reading stats |
| POST | /api/plugin/update |
Bearer | latest quotes.lua + version (OTA) |
| GET | /api/dictionaries |
Bearer | list of available Stardict dictionaries (incl. files_info with per-file sizes for download progress) |
| GET | /api/dictionaries/{slug}/file/{filename} |
Bearer | one dictionary component file, streamed |
The web UI (session-cookie login) covers everything else: / (Schreibtisch),
/books, /book/{id}, /search, /zettel, /tags, /collections,
/shared, /notifications, /dictionaries, /songs, /articles,
/hauptkasten, /account (incl. setup bundle download), /admin,
plus per-book and whole-library export under
/book/{id}/export?format=md|txt and /export?format=md|txt.
Ingest payload
{
"version": "quotes/0.6.3", // plugin version string
"created_on": 1700000000,
"device": "KindlePaperwhite4", // device label for provenance
"documents": [
{
"title": "...", "author": "...", "file": "/mnt/onboard/book.epub",
"md5sum": "...", // stable book key; falls back to file/title hash
"number_of_pages": 320,
"stats": { // optional, from KOReader's statistics + sidecar
"total_read_time": 39120, "last_read": 1700000000,
"pages": 320, "read_pages": 280, "notes": 3, "highlights": 12,
"first_read": 1690000000, "percent_finished": 0.94
},
"highlights": [
{ "text": "...", "note": "...", "page": 12, "chapter": "...",
"color": "yellow", "drawer": "lighten", "time": 1700000000 }
]
}
]
}
Roadmap / offene Ideen
Der ausführliche, gestufte Plan steht in ROADMAP.md, die
durchnummerierten Einzel-Vorhaben in BACKLOG.md. Offene Punkte:
- #34 Highlight-Zwei-Wege-Sync — KOReader-Highlights zwischen Geräten
synchron halten, kuotes als Hub (voller Zwei-Wege-Sync). Design steht in
highlight-sync-plan.md; noch nicht gebaut. - #30 E-Mail-Benachrichtigungen — User-Notifications optional per SMTP.
- Absolutic-Theme finalisieren (Branch
absolutic-theme). - calibre-Bibliothek per OPDS-Kategorien browsen (Autoren/Tags/Serien).
- „To-read"-/Wunschlisten; mehrere OPDS-Quellen (#20).
- Buchcover (kosmetisch); Volltextsuche via SQLite FTS5 (aktuell
LIKE).
See STATUS.md for the detailed state and resume notes.