# Develop Changelog
## 2026-05-09
- Reader: progress is now monotonic across devices — saved position only advances, never rewinds
- `POST /library/progress/{filename}` in `routers/reader.py` parses the incoming `cfi` as `(chapterIndex, scrollFrac)` and the currently stored row the same way, then skips the write when `new_pos <= cur_pos`
- Same `cfi` format is used for EPUB, PDF and CBR/CBZ, so one tuple comparison covers all readers
- Explicit Read/Unread actions still clear the row (`mark-read` / `mark-unread` delete from `reading_progress`), so users can deliberately reset and start over
- Reason: when reading the same book on device A (chapter 12) and then continuing on device B (chapter 15), opening device A again previously sent its stale chapter-12 cfi back to the server and overwrote the further progress; now the older position is ignored
---
## 2026-04-22 (5)
- Grabber: newly converted books now appear in the **New** view again
- Both the DB-storage and file-EPUB branches in `routers/grabber.py` set `needs_review: True` on `upsert_book` (was `False`); the New view filters on `needs_review == True`, so previously grabbed books never showed up there
- Matches the behavior of disk-scanned imports (`library.py` already sets `needs_review = True` for freshly discovered files)
---
*Released as v0.2.8 on 2026-04-22*
## 2026-04-22 (4)
- Break detection: runs of consecutive break images are now collapsed to a single break
- New helper `collapse_consecutive_breaks()` in `xhtml.py` matches 2+ consecutive `
` occurrences (with optional whitespace in between) and replaces them with one
- Applied in `normalize_wysiwyg_html()` (editor save path) and in `routers/grabber.py` on both the preview `converted_xhtml` and the per-chapter `content_html` produced during scraping
- Docs: `docs/TECHNICAL.md` updated to cover previously missing changes — `POST /api/edit/intro/{filename}` and the `title` field on file-EPUB chapter save; FlareSolverr sidecar and `BaseScraper.close()`; `AwesomeDudeScraper` uses FlareSolverr; `make_epub(include_intro=…)` and `epub_utils.build_book_info_body_html`; grabber DB flow stores Book Info as chapter 0; `"Book Info"` h1-strip skip in reader; new env vars (`FLARESOLVERR_URL`, `FLARESOLVERR_TIMEOUT_MS`, `NOVELA_PORT`, `ADMINER_PORT`); `collapse_consecutive_breaks()` helper
---
*Released as v0.2.7 on 2026-04-22*
## 2026-04-22 (3)
- Scrapers: Cloudflare "Just a moment…" challenges are now solved via a new FlareSolverr sidecar service so books on protected sites (awesomedude.org) can be scraped again
- New service `flaresolverr` in `stack/stack.yml` (image `ghcr.io/flaresolverr/flaresolverr:latest`, internal-only, on `novela-net`); `novela` service gains `FLARESOLVERR_URL=http://flaresolverr:8191/v1` and a `depends_on: flaresolverr`
- New helpers in `scrapers/base.py`: `flaresolverr_get(url, timeout_ms=None, session=None)` posts `cmd: request.get` and returns a `SimpleNamespace(text, url)` (drop-in for `httpx.Response` attributes); `flaresolverr_session_create()` and `flaresolverr_session_destroy(sid)` manage browser sessions so Cloudflare cookies stay warm across per-chapter requests (first page solves the challenge, all further chapters reuse the same browser — much faster)
- Configurable via `FLARESOLVERR_URL` and `FLARESOLVERR_TIMEOUT_MS` env vars (defaults: service DNS name and 60000 ms)
- `BaseScraper` gained an async `close()` method (default no-op) for releasing scraper-scoped resources; `scrapers/awesomedude.py` creates a FlareSolverr session in `fetch_book_info`, reuses it in all `fetch_chapter` calls, and destroys it in `close()`
- `routers/grabber.py` now wraps all scraper usage in `try/finally: await scraper.close()` so sessions are released even on errors
- Stack uses `${NOVELA_PORT}` and `${ADMINER_PORT}` (defined in `stack/novela.env` as 8099 / 8098) so host-port values don't diverge between environments
---
*Released as v0.2.6 on 2026-04-22*
## 2026-04-22 (2)
- Editor: chapter titles are now editable for file-EPUB books as well (DB books already supported this)
- Frontend (`editor.js`): the `chapter-title-input` is always shown (the read-only `header-chapter` label is hidden for both storage types), title changes mark the chapter dirty, and the title is sent in both `saveChapter` and `saveAllChapters`
- Backend (`routers/editor.py`): `POST /api/edit/chapter/{index}/{filename}` for file EPUBs now accepts `title` alongside `content`; if the title changed it calls new helper `_update_epub_navpoint_title(path, href, new_title)` which locates the matching NCX `navPoint` by content src and rewrites its `` via `rewrite_epub_entries`
---
## 2026-04-22
- Book Info page: new "Info page" button in the editor toolbar generates a gayauthors-style info page and inserts it as the first chapter
- New endpoint `POST /api/edit/intro/{filename}` builds the page from stored metadata (author, genres, sub-genres, tags, description, source, updated) and prepends it
- DB books: shifts existing `chapter_index` values up by one and inserts `"Book Info"` at index 0 (two-step negation to avoid unique-constraint violations)
- File EPUBs: writes a new `intro_.xhtml` via `make_intro_xhtml`, adds a manifest item, places the `itemref` at the start of the spine, and inserts a navPoint at the top of the NCX with renumbered `playOrder`
- Empty metadata fields are skipped
- Option A: no duplicate detection — clicking the button on a book that already has one will add a second page
- Grabber convert: DB-storage conversions now persist the Book Info page as a real stored chapter at index 0 (so it shows up in the editor and reader); EPUB-storage conversions continue to produce `intro.xhtml` via `make_epub` as before
- DB → EPUB export (`reader.py`): no longer synthesises `intro.xhtml` (`make_epub(..., include_intro=False)`) — the stored chapter 0 is the single source of truth
- Legacy DB books converted before this change have no stored info page; their exports will lack an intro page until the button is used
- `make_epub` gained an `include_intro: bool = True` parameter; when false, the `intro.xhtml` file, its manifest item, its spine `itemref`, and its NCX navPoint are all omitted (remaining `playOrder` values start at 1)
- Shared helper: `epub_utils.build_book_info_body_html(title, author, info)` — returns the inner-body HTML fragment used for DB storage, starting with `{title}
`; skips empty fields and separates description and source/updated blocks with `
`
- DB-storage info page: chapter title is `"Book Info"`; to preserve the leading `{book title}
` in its body, the leading-h-tag stripping in `reader.py` (`get_chapter_html` and the DB→EPUB export) is now skipped when `title == "Book Info"`
---
*Released as v0.2.5 on 2026-04-22*
## 2026-04-21 (2)
- Backup: separate "scanned" vs "uploaded" counters in backup_log and UI
- New migration `backup_log_scanned_files` adds `scanned_files INTEGER` column to `backup_log`
- `_run_backup_internal` now returns `(scanned_files, uploaded_count, uploaded_size)`
- `_finish_backup_log(...)` accepts `scanned_files=`; `_run_backup_job` passes the value through
- `/api/backup/status` and `/api/backup/history` return `uploaded_files` (old `files_count`) and `scanned_files`
- `backup.html`: Latest Status now shows separate "Scanned" and "Uploaded" rows; History table renamed "Files" column to two columns "Scanned" / "Uploaded" (colspan of loading/empty states updated to 8)
- Backup page: live phase indicator under the Run buttons while a backup is running
- New `#run-progress` status line is filled by `pollRunProgress()` which polls `/api/backup/progress` every 3 s
- Phase labels spelled out: `scanning library`, `uploading library objects`, `uploading snapshot`, `uploading pg_dump (may take minutes)` — so users understand why the counter sits at `N / N` during the final phases
- Polling starts on page load (via `refreshAll()`) and is re-kicked when Run Live/Dry is clicked; it auto-stops when progress reports `running=false`
---
*Released as v0.2.4 on 2026-04-21*
## 2026-04-21
- Backup: fix Dropbox `Read timed out. (read timeout=120)` error on large uploads
- `dropbox.Dropbox(...)` `timeout` raised from `120` to `300` in `routers/backup.py` (`_dbx()`, both refresh-token and legacy-token branches)
- `_DROPBOX_UPLOAD_CHUNK` reduced from `100 * 1024 * 1024` (100 MB) to `16 * 1024 * 1024` (16 MB)
- `_DROPBOX_UPLOAD_THRESHOLD` lowered to match (`16 * 1024 * 1024`) so the session upload path is used earlier
- Net effect: each chunk PUT finishes well within the socket timeout, and a stalled connection gets 5 minutes instead of 2 before erroring
---
*Released as v0.2.3 on 2026-04-21*
## 2026-04-16 (2)
- Editor: four inline formatting buttons added to the chapter editor toolbar
- **S** (subheading) — wraps selection in `` (red, bold in reader)
- **C** (chat) — wraps selection in `` (orange in reader)
- **→|** (indent) — wraps selection in `` (or `
` when the selection contains block elements)
- **[ ]** (comment) — wraps selection in `
` when the selection contains `
`, `
` also contains a "Back" button (``) and the author byline (``) - Fix: only direct `NavigableString` children of the h2 are used as the title, so link and span text is skipped ## 2026-04-08 (11) - New scraper: `TedLouisScraper` (`scrapers/tedlouis.py`) for `tedlouis.com` - Matches all `tedlouis.com` URLs; no login required - Entry point is the story index page (e.g. `?t=CWYSqpOryu2rQmT1`); raises an error when a chapter URL is used as entry point - Title from direct text nodes of `
` - Author from ` ` - Status from `` (strips "Status: " prefix) - Updated date from `` → `YYYY-MM-DD` - Chapters from all `
` elements (three columns); relative `?t=TOKEN` links resolved to absolute URLs - `fetch_chapter()`: content from ``; removes story title, chapter title and copyright blocks
## 2026-04-08 (10)
- Settings: break image upload added
- New "Break image" card on the settings page: upload a PNG/JPG/WebP as the scene break image
- Stored in the imagestore (sha256-addressed) and overwrites `static/break.png` so EPUB export uses the same image
- `app_settings` extended with `break_image_sha256` and `break_image_ext` columns (migration `migrate_app_settings_break_image`)
- New endpoints: `POST /api/app-settings/break-image`; `GET /api/app-settings` now also returns `break_image_url`
- Preview of the current break image visible on the settings page
## 2026-04-08 (9)
- Grabber: break image fix for DB-stored books
- Break images (`
`) contain a relative EPUB path that does not exist in the DB context
- Fix: `storage_mode` determined earlier in `_run_scrape`; for DB mode `/static/break.png` is passed as `break_img_path` to `element_to_xhtml`, for EPUB mode `../Images/break.png` is kept
- Additional fix: when a break image is detected in the image loop, the parent (``) is replaced with `
`; this allows `element_to_xhtml` to correctly detect the break after the image loop
## 2026-04-08 (8)
- New scraper: `IomfatsScraper` (`scrapers/iomfats.py`) for `iomfats.org`
- Matches all `iomfats.org` URLs; no login required
- Entry point is a chapter URL (e.g. `.../grasshopper/justhitsend-part1/justhitsend01.html`); automatically navigates to the author page to fetch all metadata and chapters
- Author page as entry point: raises a user-visible error message
- Detects two structures on the author page:
- Single story: outer `
` instead of decomposing the `
` = book title; chapters directly in `
` - Multi-part series: outer `
` = series name; nested `
` = book title per part; chapters in sub-`
` - Series index from folder name: `*-part{N}` or `*-{N}` → `series_index_hint` - Publication status from ``; removes `
[...]
` after book title - `fetch_chapter()`: content via ``/`
` headings, chapternav divs, footer elements ## 2026-04-08 (7) - New scraper: `CodeysWorldScraper` (`scrapers/codeysworld.py`) for `codeysworld.org` - Matches all `codeysworld.org` URLs; no login required - Title from `
`; author from `
` ("by …"); fallback to URL slug (`/{author}/{category}/filename`) - Category from URL path → tag (e.g. "Remembrances") - Single-file stories (no chapter links): the page itself is the only chapter - Multi-chapter: links to `.htm`/`.html` files in the same directory (audio/image links skipped) - `fetch_chapter()`: removes all `
`/`
` headings, navigation links ("Back to …"), audio links (`.mp3`), mailto links - Nifty scraper: category/subcategory moved from `genres`/`subgenres` to `tags` ## 2026-04-08 (6) - NiftyNewScraper: chapter extraction made more robust - `fetch_chapter()` now first tries the standard chapter HTML (`{url}`) and reads `
` directly - Fallback added for Next payloads with escaped paragraphs (`\u003cp...\u003c/p`) via `_extract_escaped_html_paragraphs()` - Last fallback remains `?_rsc=1`: first `_parse_rsc_paragraphs()`, then escaped-paragraph fallback again - Nifty (classic + new): standard boilerplate no longer visible in reader, but preserved in chapter - Lead/tail detection added for common blocks (e.g. `NOTICE This is a work of fiction…`, `If you enjoy this story…`, donate text) - Detected intro/closing boilerplate removed from visible paragraphs - Removed text stored as invisible HTML comment: - `` - `` - Detection also works when text contains inline HTML (e.g. `` or `