- Player-Sync: Aktion 'sleep' (Minuten, -n Folgen, 0 aus); Reports schicken
sleep {left_ms, episodes} mit, der State zeigt ihn (Migration 0016)
- Browser-Host: Nachtfenster sleep_from/sleep_to, Automatik auch beim
Radiostart (auch in diora desktop), Timer des anderen Geräts zählt mit,
ein Timer, der stundenlang nicht lief, verfällt
- Eingabefenster (customPrompt) liegt über Player und Menüs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- EpisodeProgress.discarded_at (Migration 0015); POST /podcasts/episodes/discard/
{"episode_ids": [...]}: aus der Warteschlange (_dequeue, auch von queue/remove benutzt)
und aus "Neu" (dismissed), discarded_at = jetzt; gehört, Stelle, Stern und updated_at
bleiben; fremde Folgen werden ausgelassen (count)
- state/ liefert "discarded": [{id, at}] der letzten 30 Tage: jedes Gerät löscht seine
ältere Datei; wieder eingereiht (_enqueue) wird discarded_at gelöscht
- Browser-Host: podcastDiscard (pausiert geladene Folge verlässt den Player, laufende spielt
zu Ende), podcastPlayEpisodeAt (Kapitel auf der Folgen-Seite)
- static/podcasts aus diora-android ee1a910 (Folgen-Seite, Folge löschen, Datei löschen,
Wischtexte und Icons, Ziehgriff)
- Tests: DiscardTests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setzt eine Seite window.DIORA_PODCAST_HOST, arbeitet podcasts-web.js trotz
vorhandenem DioraNative als ihr Podcast-Host:
- dock: 'existing' nimmt native-radio.js' #nui-dock statt einer Leiste in der
Web-Radioleiste; die Podcast-Seite lädt erst nach DOMContentLoaded
- styles: 'ui-only' nur podcast-ui.css, ohne das Browser-Layout (html.dpw)
- radio: false die Radio-Hooks (__dioraRadioNow …) kommen von native-radio.js;
der Sender legt die Folge weiterhin beiseite, Radio-Stopp meldet der Sync weiter
- deviceName/deviceKind wie der gemeinsame Player das Gerät führt
Ohne die Variable ändert sich im Browser nichts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ab 900 px Fensterbreite starten Bücher mit 20 px, Zeilenhöhe 1.6, 90 Zeichen
Breite und Gewicht 375; Handys behalten 16 / 1.8 / 65 / normal.
- Gespeicherte Einstellungen tragen jetzt v: 2. In älteren gelten Werte, die
genau dem alten Standard entsprechen, als nie gewählt und weichen dem
heutigen Standard; bewusst geänderte Werte bleiben.
- Der Breiten-Regler geht bis 140 statt 90 Zeichen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- state/ liefert inbox_total (alle neuen Folgen) und inbox_limit neben der
gekürzten Liste; Test dazu.
- Browser: Startseite (nui_start_page) beim Öffnen, „Rückgängig“-Leiste über
der Radioleiste, helle Ansicht für die neuen Teile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der Chip "Benachrichtigen" auf der Podcast-Seite setzt notify auch im
Browser (die Mitteilungen kommen von den Telefonen). Die Einstellung
"Im Hintergrund abgleichen" gibt es nur in der App.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PodcastFeed.notify (Standard aus) wie auto_download: in feeds/ und
state/, änderbar über feeds/<id>/settings/ (nur true/false). Die Geräte
melden damit neue Folgen dieses Podcasts. Der AntennaPod-Import übernimmt
Feeds.episode_notification (an -> notify an; aus, AntennaPods Standard,
lässt dioras Einstellung stehen; ältere Backups ohne die Spalte: aus).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bisher lud die Seite bei einer neuen Version sofort neu – auch mitten im
Radio oder Podcast. Jetzt erst, wenn nichts spielt, kein Buch offen ist
und kein Textfeld den Fokus hat. Der Browser heißt im Player-Sync kurz
„Chrome · Windows“.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Player-Leiste sitzt in der Radio-Leiste an der Stelle von „läuft
gerade“: am Desktop eine Zeile (Player, Lautstärke, Fokusmodus, Timer),
am Handy zwei; die Fortschritts- bzw. Live-Linie läuft über die ganze
Leiste. Der Fokusmodus zeigt den Sender weiter groß. sw v56.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Podcast-Leiste und der Vollbild-Player zeigen auch den Sender (Logo,
„● Live“, Stopp behält den Sender, speichern, Sleep-Timer, Sender
wechseln); die Radio-Leiste behält nur Lautstärke, Votes, Fokusmodus und
Timer. Podcast-Oberfläche aus diora-android e6761d0, sw v55.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--replace bestellt Podcasts ab, die das Backup nicht hat, behält nur
AntennaPods Warteschlange und lässt AntennaPod auch über spätere
Änderungen in diora gewinnen. Die Oberfläche bringt den gemeinsamen
Player und die Download-Übersicht (nur in der App sichtbar). sw v54.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folgen archivierter Podcasts (neue Folgen ignorieren), die AntennaPod nie
abgerufen hat, verlassen „Neu“, sofern sie vor dem Backup erschienen
sind; --quiet-diora-only macht das auch für Abos, die nur diora hat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folgen, die in AntennaPod auf dem Handy lagen (FeedMedia.downloaded > 0:
Download-Zeitpunkt, bei älteren Versionen 1), merkt sich der Import in
PodcastSettings.import_downloads (episode_id + Größe, neueste zuerst).
Fehlen sie in diora noch, werden sie angelegt (nicht neu). Ein echter Lauf
ersetzt die Liste, der Probelauf schreibt nichts; die Zusammenfassung hat
eine Zeile „Downloads“.
GET /podcasts/import-downloads/ liefert die Folgen wie Einträge von „Neu“
plus size, POST …/clear/ leert die Liste, state/ meldet Anzahl und Bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audio bleibt bevorzugt, wenn eine Folge beides hat; Endungen werden am
Pfad erkannt (Query-Strings stören nicht mehr). refresh_feeds --full
liest alle Feeds ohne ETag neu ein; dabei gefundene Folgen, die vor dem
letzten Abruf erschienen sind, kommen in die Liste, nicht nach „Neu“.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Neuer Befehl `manage.py import_antennapod <backup.db> --user <name> [--dry-run]
[--global-auto-download on|off] [--backup-date ...]`: übernimmt aus einer
AntennaPod-Datenbanksicherung die Abos (Abgleich über normalisierte URL,
fehlende werden angelegt, lokale übersprungen), die Podcast-Einstellungen
(Tempo, Intro/Outro, Lautstärke, Filter mit Phrasen in Anführungszeichen,
Mindestlänge, neue Folgen, „nicht aktualisieren“ -> ignorieren, automatischer
Download), den Zustand jeder Folge mit Zustand (gehört, neu, Position,
Favorit, Zeitpunkt), die Warteschlange (AntennaPods Reihenfolge zuerst) und
nimmt bekannte, dort nicht neue Folgen aus „Neu“. Neuere Änderungen in diora
bleiben, Favoriten werden vereinigt. Alles in einer Transaktion; ein zweiter
Lauf ändert nichts mehr.
- PodcastFeed.auto_download (Einstellung pro Podcast, in feeds/, state/ und
feeds/<id>/settings/) und PodcastSettings.auto_download_default (in settings/
und state/), mit dem neue Abos (Hinzufügen, OPML, gpodder, Import) starten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seit die Podcast-Modelle JSON-Felder haben, öffnet die Systemprüfung die
Datenbank (unterstützt SQLite JSON?). Im CI fehlt data/ – die Prüfung
brach mit „unable to open database file“ ab, die Tests liefen gar
nicht. In Produktion ist data/ ein Volume, dort war nichts betroffen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migration als 0009 hinter 0008_feed_http_cache eingereiht; Tests an die
neue _fetch_feed-Schnittstelle (FeedFetch) angepasst. 202 Tests grün.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ohne Kapitel im Feed (kein PSC, keine chapters_url oder dort nichts)
liest die Folgen-Detailansicht einmalig den ID3v2-Tag am Anfang der
Audiodatei (Range-Anfrage, höchstens 4 MB, Weiterleitungen mit
SSRF-Prüfung) und übernimmt dessen CHAP-Frames (v2.3/v2.4,
Unsynchronisation, erweiterter Header, Synchsafe-Größen, CTOC-Reihenfolge).
Eigener Parser nach der ID3-Spezifikation; Ergebnis wird wie die
JSON-Kapitel zwischengespeichert, vorübergehende Fehler nicht.
- Neu: GET /podcasts/episodes/<id>/transcript/ holt das beste Transkript
(JSON vor VTT vor SRT, Deutsch bzw. ohne Sprachangabe vor Englisch),
zerlegt es in Abschnitte {start, end, speaker, text}, fasst kurze
Schnipsel desselben Sprechers bis zum Satzende bzw. ~30 s zusammen und
speichert das Ergebnis (eigene Tabelle EpisodeTranscript). HTML: 404
{"error": "unsupported"}.
- Folgen-Detail liefert zusätzlich has_transcript.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migrationen hintereinander statt nebeneinander: 0007_feed_settings,
dann 0008_feed_http_cache. Tests der Podcast-Regeln an die neue
_fetch_feed-Schnittstelle (FeedFetch) angepasst; Feeds-Liste trägt
Einstellungen und Fehlerfelder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PodcastFeed (Migration 0007_feed_settings): playback_speed (null = Tempo
des Geräts), skip_intro, skip_outro, volume (0.2/0.5/1.0/1.5/2.0/2.5),
new_episodes (inbox/queue/ignore), include_words, exclude_words,
min_duration. Abos mit auto_queue bekommen new_episodes='queue';
auto_queue bleibt gleich (new_episodes == 'queue'), auch über
set-auto-queue/ und beim Speichern im Code.
- POST feeds/<id>/settings/: beliebige Teilmenge, prüft jeden Wert (400 mit
Feldname, dann wird nichts geändert), nur eigene Abos (sonst 404);
Antwort sind alle Einstellungen des Abos. feeds/ und state/ liefern sie
für jedes Abo mit.
- Neue Folgen beim Aktualisieren: was am Filter scheitert (keins der
Wörter, ein Ausschlusswort im Titel, kürzer als die Mindestlänge) und
bei 'ignore' alles verlässt sofort „Neu“ (nicht gehört); 'queue' reiht
an der auto_queue_position ein; 'inbox' wie bisher. Beim ersten Abruf
zählt weiter nur die neueste Folge.
- PodcastSettings.keep_sorted ('', new, old, short, long, feed): danach
wird die ganze Warteschlange nach jedem Einreihen, jedem Umsortieren und
beim Einschalten neu sortiert; settings/ und state/ liefern den Wert.
- Tests für alles davon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Bedingter Abruf: ETag/Last-Modified werden nach einer 200 gespeichert und
als If-None-Match/If-Modified-Since mitgeschickt. Bei 304 wird nichts
geparst und keine Folge geschrieben, nur last_refreshed_at aktualisiert und
der Fehler gelöscht. Teilen sich mehrere Abos eine URL, gehen die Validatoren
nur mit, wenn alle schon einmal abgerufen wurden und dieselben haben.
- Fehler (Netz, HTTP >= 400, kein Feed) landen in last_error, last_error_at
und error_count; ein erfolgreicher Abruf setzt sie zurück. Feed-Liste und
/podcasts/state/ liefern die drei Felder mit.
- Dauerhafte Weiterleitungen (301/308 am Anfang der Kette) ändern rss_url,
außer der Nutzer hat die neue URL schon abonniert; 302/307 ändern nichts.
- Backoff im stündlichen Job: ab 3 Fehlern in Folge nur alle 2, 4, 8, 16,
höchstens 24 Stunden. „Alle aktualisieren“ und --feed-id versuchen es
trotzdem (max_age gilt weiter).
- _fetch_feed liefert jetzt ein FeedFetch-Objekt bzw. wirft FeedFetchError.
- Migration 0007_feed_http_cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Eingereihte und angefangene Folgen verlassen die Inbox (ausgeblendet,
nicht gehört), statt dort neben der Warteschlange zu stehen.
- Gehört heißt: höchstens 30 s vor dem Ende, wie in den Apps, statt bei
90 % (bei 2 h waren das 12 Minuten vor Schluss). Gilt auch für den
gPodder-Sync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Folge wurde nur über die Audio-Adresse gesucht, über alle Nutzer
hinweg. Haben zwei Nutzer denselben Podcast, landete die Stelle bei der
Folgen-Zeile des anderen (der eigene Fortschritt, aber an der falschen
Folge) und kam beim eigenen Abo nie an. Jetzt nur unter den eigenen
Abos gesucht; mit Test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- EpisodeProgress.favorite (Migration 0006) und POST episodes/<id>/favorite/
(setzen oder umschalten; ändert updated_at nicht, damit Verlauf und
Statistik nicht verfälscht werden)
- GET episodes/: alle Folgen aller Abos, Filter all/unplayed/in_progress/
played/favorites/history, feed_id, q, offset/limit/total; zwei Abfragen
pro Seite, ohne Shownotes
- Folgendetails liefern zusätzlich favorite und last_played_at
- GET stats/: Hörzeit gesamt und pro Podcast, optional nur die letzten N Tage
- GET feeds/export.opml: Abos als OPML 2.0 (lässt sich wieder importieren)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seit die Shownotes content:encoded nehmen (bis 50 KB pro Folge), würde
die eine Anfrage der Apps mit 200 Inbox-Folgen sehr groß. state/ lässt
sie weg; die Apps holen sie pro Folge über episodes/<id>/. inbox/ für
das Web liefert sie weiter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Feeds liefern jetzt Podlove-Kapitel (psc:chapters), Podcasting-2.0-Kapitel
(podcast:chapters, JSON-URL) und Transkripte (podcast:transcript, auch
mehrere); als Beschreibung wird content:encoded bevorzugt.
- Bekannte Folgen werden beim Aktualisieren nachgezogen, aber nur die Zeilen,
die sich wirklich geändert haben.
- Neuer Endpunkt GET /podcasts/episodes/<id>/ mit bereinigten Shownotes,
Zeitmarken, Kapiteln, Transkripten und Fortschritt. JSON-Kapitel werden
einmal serverseitig geholt (nur öffentliche Adressen, max. 1 MB) und an
der Folge gespeichert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Beim ersten Abruf eines Feeds (Abonnieren, OPML-Import, gpodder) kam der
ganze Rückkatalog als „neu“ in die Inbox (bei Lage der Nation 200
Folgen). Jetzt zählt nur die neueste; die älteren sind ausgeblendet,
nicht gehört, und stehen weiter in der Folgenliste des Podcasts. Auch
automatisches Einreihen nimmt beim ersten Abruf nur diese eine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Warteschlange
- PodcastSettings: wohin neue Folgen kommen, getrennt für Hinzufügen von
Hand (auch beim Abspielen einer Folge außerhalb der Warteschlange) und
für automatisch eingereihte neue Folgen: ans Ende, an den Anfang oder
nach die laufende Folge. Voreinstellung wie bisher (Hand: Ende,
automatisch: Anfang). GET/POST /podcasts/settings/.
- queue/add nimmt auch mehrere Folgen (episode_ids), eine Position
(where) und die laufende Folge (current_episode_id); ohne Angabe gilt
die zuletzt angehörte, nicht fertige Folge als laufende.
- Positionen werden lückenlos neu durchnummeriert.
Feeds
- POST /podcasts/feeds/refresh-all/ aktualisiert alle Feeds des Nutzers
auf dem Server: Downloads parallel, jede Feed-Adresse nur einmal (auch
über Nutzer hinweg), mit Timeout; kürzlich aktualisierte werden
übersprungen. Der stündliche Cron-Job nutzt dasselbe.
- GET /podcasts/state/ liefert Abos, Warteschlange, Inbox und
Einstellungen in einer Antwort (für die Apps).
- Web: „↻ All“ ist eine Anfrage statt einer pro Feed; die stündliche
Schleife im Browser lädt nur noch die Listen neu, statt jeden Feed
selbst anzustoßen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Eine http(s)-Adresse im Suchfeld wird nicht gesucht, sondern als Treffer
mit Play und Save (Name wählbar) angeboten. Der Query-String (oft ein
Zugangstoken) wird nicht angezeigt, aber mitgespeichert.
- toggleFav gab es zweimal; die zweite Fassung verdeckte die erste und
brauchte den Button als Argument, den die serverseitig gerenderten Zeilen
nicht übergeben. Folge: Favorit auf dem Server gesetzt, Stern blieb leer,
der nächste Klick nahm ihn wieder weg. Jetzt eine Fassung für beide Fälle,
inklusive Umsortieren (Favoriten zuerst, wie der Server).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
playStation() öffnet http-Sender auf einer https-Seite in einem neuen
Fenster. Native Hüllen (diora Android, diora desktop), die den Stream
selbst abspielen, setzen window.DIORA_PLAYS_HTTP_STREAMS und überspringen
das.
stopPlaySession() wurde von playStation() ohne await aufgerufen und
setzte currentPlayId erst nach dem fetch auf null – beim Senderwechsel
also die frisch gesetzte ID der neuen Session. Die ID wird jetzt vor dem
await übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native Clients (diora desktop) haben kein CSRF-Cookie; ein Browser hängt den
Authorization-Header nie von selbst an, daher ist das sicher. Nötig z. B. für
den M3U-Import, der nicht csrf_exempt ist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
openBook leert #reader-content und hängt die Kapitel über mehrere awaits
an. Ein zweiter Aufruf dazwischen (zweites Tippen, während ein langsamer
E-Reader noch entschlüsselt) hing das Buch doppelt an. Daraus gespeicherte
Anker lagen hinter dem echten Buchende (tolino: Block 1491 bei 1206
Blöcken) und schlugen beim Server-Vergleich "weitester Stand gewinnt"
jede echte Position: Fortschritt wurde nicht mehr gespeichert.
- openBook läuft nur einmal gleichzeitig (gleiches Buch: auf den laufenden
Aufruf warten; anderes Buch: danach öffnen).
- Aus einem doppelt gerenderten Dokument wird kein Fortschritt gespeichert.
- Ein gespeicherter Anker, der nicht zum Buch passt, führt zum Textanteil
(scroll_fraction) statt an den Anfang, und die nächste Speicherung
überschreibt ihn (force), damit der Fortschritt wieder läuft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>