84 KiB
84 KiB
Develop Changelog
2026-06-12 — Visual editor: broaden schema & fidelity check against real library content
Changed
- Tested the round-trip safety gate against every chapter of the actual DB library and broadened the visual editor so Visual mode is available for the overwhelming majority of real content instead of being refused on common, lossless markup. Across the sampled DB books (~185 chapters) Visual mode now opens for 100% of chapters; only genuinely lossy or malformed markup is still blocked.
- Schema broadened to cover conventions that real chapters actually use (all now round-trip losslessly): the block variants of subheading and chat (
<div class="subheading">…</div>,<div class="chat">…</div>— previously only the inline<span>marks were modelled), block indent (<div style="padding-left: 40px;">…</div>), all heading levels h1–h6 (was h2/h3 only; the Book Info page uses<h1>, and some books use<h4>separators), plain<hr>rules (re-enabled alongside the<center><img>scene break), links (<a href>, with rel/target injection disabled so links stay byte-identical), and preservation of arbitraryclassattributes on paragraphs/headings (e.g. the generated<p class="author">). - Round-trip comparison made tolerant of the editor's lossless normalisations, so they no longer read as "unsafe": inline formatting is now compared as an unordered mark set per text run (so
<em><strong>x</strong></em>and<strong><em>x</em></strong>are equal, empty marks like<strong></strong>are ignored, and adjacent identical marks merge), and loose inline content sitting directly under<body>is compared as if wrapped in a paragraph (matching how the editor wraps it). Genuine loss is still blocked — verified by guardrail tests that unknown tags (<table>,<font color>), standalone<img>, unknown<div class>wrappers, and whole-xhtml EPUB documents all remain "unsafe".- Files:
containers/novela/editor-src/extensions.js(addedSubheadingBlock,ChatBlock,IndentBlock,ClassPreserve);containers/novela/editor-src/index.js(extension list, heading levels, Link, mark-set canonicaliser with body-level loose-inline wrapping,roundtripDebugdiagnostic export);containers/novela/editor-src/package.json(+@tiptap/extension-link); rebuiltstatic/editor-bundle.js; new dev harnesseseditor-src/{diagnose.mjs,test-commands.mjs}and expandededitor-src/test.mjs. No backend oreditor.js/editor.html/editor.csschanges in this round.
- Files:
2026-06-12 — Chapter editor: optional visual (WYSIWYG) mode alongside Monaco
Added
- The chapter editor now has an optional Visual (WYSIWYG) mode next to the existing Monaco HTML-source editor, ported from the Tiptap editor built in novela-ng and adapted to Novela's own markup conventions. A Visual / HTML toggle button in the toolbar switches between the two; Monaco remains the default and the backing source of truth.
- Tiptap is bundled, not CDN-loaded. A new
containers/novela/editor-src/npm project bundles Tiptap + the custom extensions into a single committed file,static/editor-bundle.js(window.NovelaVisual), loaded by the editor page with a plain<script>tag — the runtime frontend stays build-less. Rebuild withcd containers/novela/editor-src && npm install && npm run build.node_modulesis git-ignored; the bundle is committed. - Novela conventions modelled as a real ProseMirror schema so they apply and round-trip as formatting instead of raw tags: subheading (
span.subheading), chat (span.chat), author comment (div.novela-comment), indented paragraph (<p style="padding-left: 40px;">), and scene break (<center><img src="…break.png" style="height:15px;"/></center>— the original imgsrcis preserved, so both DB/static/break.pngand on-disk EPUB../Images/break.pngbreaks round-trip exactly). Standard bold/italic/underline/superscript/subscript, H2/H3 headings, and bullet/numbered lists are also available as visual-only toolbar buttons. - Detect & block (no silent data loss). ProseMirror drops any markup its schema doesn't model, which would silently strip formatting from arbitrary EPUB/DB HTML. Switching into Visual mode is therefore gated by a round-trip safety check (
NovelaVisual.roundtripSafe): the chapter is fed through the editor and the result is compared, canonicalised, against the original (ignoring insignificant formatting differences such as attribute order, whitespace, self-closing style, andb/strong&i/emequivalence). If anything — a tag, attribute, or text — would be altered or dropped, Visual mode is refused for that chapter and editing stays in HTML mode. In practice this means Visual mode is offered for clean, convention-conforming content (typically DB books) and declined for heterogeneous EPUB markup or whole-xhtml documents, so a book's existing formatting can never be quietly destroyed. - The semantic toolbar buttons (Break, S, C, →|, [ ]) route to the corresponding Tiptap commands in Visual mode and to the existing Monaco text-wrapping in HTML mode. Visual edits participate in the existing per-chapter dirty-tracking, Save / Save-all, chapter switching, add/delete, and Find & Replace flows; the visual content is synced back into Monaco (the backing store) at every save/switch/replace boundary, and the safety gate is re-evaluated whenever a chapter is loaded. Ctrl/Cmd+S also saves while editing visually.
- Files: new
containers/novela/editor-src/{package.json,build.mjs,index.js,extensions.js,test.mjs,test-commands.mjs,test.html,.gitignore}; new committedstatic/editor-bundle.js;templates/editor.html(bundle script, toggle button, visual-only toolbar group,#visual-pane);static/editor.css(visual pane, toolbar and convention styling);static/editor.js(pane abstraction,toggleVisual/mountVisual/syncVisualToMonaco/refreshVisualAfterLoad, boundary syncing, toolbar routing). No backend changes — chapter save is unchanged and does not sanitise, so the editor's clean output is stored as-is.
- Tiptap is bundled, not CDN-loaded. A new
2026-06-03 — Reader: Page Up/Down scroll within the page
Changed
- In the reader, Page Up / Page Down no longer switch chapters — they now fall through to the browser's default in-page scrolling. Chapter navigation is still bound to the arrow keys (Left/Right). Previously both the arrow keys and the page keys triggered chapter navigation, so accidentally pressing Page Down while reading jumped to the next chapter and lost the reading position.
templates/reader.html: thekeydownhandler now matches onlyArrowRight/ArrowLeftfornavigate(±1);PageDown/PageUpremoved from the bindings.
Released as v0.2.14 on 2026-06-03
2026-06-01 — Full Database Restore: live progress + download-to-local first
Added
- Full database restores (Dropbox, local and upload) now run as a background task with live byte-level progress, polled by the Backup page, instead of a silent blocking request. This matters for large (e.g. ~1 GB) production databases where the restore previously showed nothing for minutes.
- Phases reported and shown with a percentage / size: downloading (Dropbox only) → safety snapshot → resetting schema → loading → done / rolled back / failed.
- Load progress is derived from feeding the dump to
psqlover stdin in chunks; pipe back-pressure makes the byte counter track psql's real progress closely.
- Dropbox restore now downloads the dump to the local store first, then restores from that file — so the restore no longer depends on the network connection mid-load, and a reusable local copy is kept for token-free restores. (Previously the Dropbox dump was streamed straight into memory and not persisted.)
routers/backup.py:RESTORE_PROGRESSstate +_restore_prog;GET /api/backup/restore/progress;_apply_pg_dump_file(stdin-streamed load with progress, stderr captured to a temp file to avoid pipe deadlock),_pg_dump_safety_to_local(safety snapshot with live size),_download_dropbox_to_file,_restore_worker_sync,_start_restore(single restore at a time). The three restore endpoints now start the background job and return{started: true};_run_pg_restore/_apply_pg_dump(bytes-based) were replaced.- Local dump retention now sorts by mtime (robust across
pre-restore-…and downloaded dump names). templates/backup.html: sharedpollRestoreProgresswith phase labels and percentage/size; the three restore buttons start the job then poll; progress resumes on page reload if a restore is still running.
2026-06-01 — Sidebar version is display-only
Changed
- The build version at the bottom of the sidebar is no longer a clickable link to the changelog — it is now a plain text display. It exists only to show that the running build number has incremented (so it's clear the new code is active).
templates/_sidebar.html:<a href="/changelog" class="sidebar-version">→<span class="sidebar-version">.static/sidebar.css: dropped the link/hover styling; addedcursor: defaultanduser-select: none.
2026-06-01 — Local pre-restore snapshots + token-free / upload restore
Added
- Every restore now writes its pre-restore safety dump to a local store on the config volume (
CONFIG_DIR/postgres_dumps/, namedpre-restore-…, with snapshot-equal retention), so the database can be restored or rolled back without a Dropbox token. Regular backups are intentionally left Dropbox-only — production backs up hourly, so writing every backup to disk would fill the volume.routers/backup.py:LOCAL_DUMP_DIR, helpers_save_local_dump,_list_local_dumps,_enforce_local_dump_retention,_resolve_local_dump;_run_pg_restorepersists the safety dump locally (and prunes by retention) before the destructive load._run_backup_internalis unchanged (no local copy).
- New Local Database Restore card on the Backup page with two token-free paths:
- Restore from a locally stored dump —
GET /api/backup/local/dumps,POST /api/backup/local/restore. - Upload a
.sqldump (e.g. downloaded manually from dropbox.com) and restore it —POST /api/backup/upload-restore. The upload itself is not persisted; the automatic pre-restore safety snapshot covers the local copy. - Both reuse the safe restore path (pre-restore safety dump + automatic rollback) and require
psql. templates/backup.html: new card with a local-dump selector, an upload field, double-guarded restore buttons, andfmtBytessizes.
- Restore from a locally stored dump —
2026-06-01 — Full Database Restore: safety dump + automatic rollback
Fixed
- Full Database Restore could leave the database empty if the dump failed to load: the restore dropped and recreated the public schema first and only then applied the dump, so any failure during the load (e.g. a header
transaction_timeouterror withON_ERROR_STOP) wiped all data with nothing to fall back to.routers/backup.py: split the load into_apply_pg_dump;_run_pg_restorenow takes a safetypg_dumpof the current database before the destructive load and, if applying the requested dump fails, automatically rolls back to that safety snapshot. A failed restore therefore no longer leaves an empty or broken database.
2026-06-01 — Full Database Restore: tolerate PostgreSQL version mismatch
Fixed
- Full Database Restore failed with
ERROR: unrecognized configuration parameter "transaction_timeout"when the dump was produced by a newerpg_dump(PostgreSQL 17+, which emitsSET transaction_timeout = 0;in the header) but restored into an older server (<17) that doesn't know that session parameter. The restore ran withON_ERROR_STOP=1and aborted on that harmless header line.routers/backup.py(_run_pg_restore): the dump is now applied withoutON_ERROR_STOP; stderr is inspected afterwards via_real_restore_errors, which ignores benign "unrecognized configuration parameter" errors but still fails the restore on any otherERROR:line. The schema-reset step keepsON_ERROR_STOP=1(it is our own controlled SQL).
2026-06-01 — Backup/Restore: database-stored books are now restorable
Fixed
- Database-stored books (
storage_type='db', syntheticdb/...filenames) could not be restored through the Backup → Restore option. The restore UI only listed and restored files from the on-disk library object store, but db-books have no file on disk — their content lives entirely in PostgreSQL (book_chapters+libraryrow +book_tags+library_cover_cache). They were captured only in the fullpg_dump, which the UI offered no way to restore. As a result db-books never appeared in the restore list and were effectively unrecoverable per-book.
Added
- Per-book db restore. The backup writer now serializes each database book to JSON (library row, all chapters, tags, and the cached cover) and stores it in the same content-addressed Dropbox object store used for file books, referenced from the snapshot with a
"storage": "db"marker. Such books now appear in the Restore table (format DB) and can be restored individually; restore re-inserts the library row, chapters, tags and cover into PostgreSQL. Inline chapter images are unaffected — they already live on disk underlibrary/images/and are backed up as ordinary files.routers/backup.py: new_db_book_filenames,_serialize_db_book,_restore_db_book,_entry_storagehelpers; db-books serialized during_run_backup_internal;_download_and_restorebranches on storage type;/api/backup/snapshots/{name}/filesnow reportsstorageand computesexists_locallyfor db-books from thelibrarytable.- Note: db-books only appear in snapshots created after this change. Older backups still contain them only in the
pg_dump— recover those via Full Database Restore below.
- Full Database Restore. New Backup-page card and endpoints to restore the entire PostgreSQL database from any Dropbox
pg_dump. This recovers everything (all db-books, reading progress, tags, settings) from existing backups too. It is destructive: the public schema is dropped and recreated before the dump is applied, so any plain dump (with or without--clean) restores cleanly. Guarded behind a double confirmation in the UI.routers/backup.py:_psql_base_args,_run_pg_restore,_list_pg_dump_paths; endpointsGET /api/backup/postgres/dumpsandPOST /api/backup/postgres/restore; health endpoint now also reportspsql_available/psql_path.templates/backup.html: db-books shown with DB format and "in library / not in library" status in the restore table; new "Full Database Restore" card with dump selector, double-confirm, andpsqlhealth row.
Released as v0.2.13 on 2026-06-01
2026-06-01 — Reader: Sepia reading theme
Added
- The reader now offers a Sepia theme alongside the existing Dark theme, for easier long-form reading (warm paper background with dark brown text instead of light-on-black). New Theme toggle (Dark / Sepia) at the top of the reading settings drawer.
static/theme.css: added a:root[data-theme="sepia"]palette overriding--bg,--surface,--surface2,--border,--accent,--accent2,--text,--text-dim,--text-faint,--success,--warning,--error. Also added a--shadow-ringvariable (light hairline in dark, dark hairline in sepia) so swatch rings read correctly in both themes.templates/reader.html: theme toggle buttons +.theme-btnstyling; an inline head script applies the saved theme before paint to avoid a flash; the Text colour row now has two theme-specific swatch sets (light tints for dark, dark brown tints for sepia), toggled by visibility.- JS:
applyTheme()/loadTheme()set thedata-themeattribute and persist tolocalStorage(reader-theme). Text colour is now stored per theme (reader-text-colour-dark/reader-text-colour-sepia); the old singlereader-text-colourkey is migrated into the dark slot.init()callsloadTheme()(which loads the active theme's text colour). - Replaced two hard-coded
rgba(255,255,255,...)values in the reader CSS (swatch ring, series-button border) with theme variables so they don't show as white halos in sepia.
2026-05-31 — Build version in the sidebar
Added
- The sidebar now shows the running build version at the bottom (e.g.
v0.2.11for releases,v0.2.11.3for dev builds), linking to the changelog page.- New
containers/novela/version.pyexposesdisplay_version(). The semantic release version stays the single source inchangelog.py(CHANGELOG[0]["version"]); aBUILDsegment is appended for dev builds. shared_templates.pyregistersapp_versionas a Jinja global;_sidebar.htmlrenders it as a.sidebar-versionlink styled insidebar.css.main.pyadds a/api/versionendpoint returning{"version": display_version()}.
- New
2026-05-31 — Find & Replace: scope option
Added
- Editor Find & Replace: new Current chapter only checkbox in the modal options. When checked, the search/replace runs against the currently open chapter instead of every chapter in the book.
templates/editor.html: added therp-currentcheckbox next to Regex/Case sensitive; modal title changed from "Find & Replace — all chapters" to "Find & Replace" since scope is now selectable.static/editor.js(replaceInAllChapters()): readsrp-current; builds atargetslist of either justcurrentCh()or allchapters, and iterates that. Shows an error ("No chapter open.") if Current-chapter-only is selected with no open chapter. Result message reads "… in current chapter" for the single-chapter case.- Default is unchecked, so the existing all-chapters behaviour is preserved.
Released as v0.2.12 on 2026-06-01
2026-05-10
- Reader: subheading/chat styling now also wins when the wrapper contains block elements with their own color rule (e.g.
<div class="subheading"><p>…</p></div>)- Previously the
<p>/<h*>rules inreader.html(#chapter-content p { color: var(--text); }, etc.) had the same specificity as.subheadingand applied more directly to the inner element, so the parent's color was effectively overridden — the wrapped paragraph stayed in the default text color - CSS now applies the rule to the wrapper and all descendants:
#chapter-content .subheading, #chapter-content .subheading * { … }(and the same for.chat) - Reason: bug reported after v0.2.10 —
.chatworked because it wraps plain inline text inside a<p>, but.subheadingwrapping a<p>lost its color
- Previously the
Released as v0.2.11 on 2026-05-10
Released as v0.2.10 on 2026-05-10
2026-05-09 (2)
- Editor / Reader: subheading (S) and chat (C) styling now also applies when the wrapper is a
<div>instead of a<span>wrapSpan()ineditor.jsfalls back to<div class="…">whenever the selection contains a block element (<h1..6>,<p>,<div>, etc.) so the resulting HTML stays valid; the previous CSS inreader.html(#chapter-content span.subheading/span.chat) only matched the<span>form, so anything wrapped around or inside a heading silently lost its color/weight- CSS selectors are now class-only (
#chapter-content .subheading/.chat), matching both span and div wrappers
2026-05-09
- Reader: progress is now monotonic across devices — saved position only advances, never rewinds
POST /library/progress/{filename}inrouters/reader.pyparses the incomingcfias(chapterIndex, scrollFrac)and the currently stored row the same way, then skips the write whennew_pos <= cur_pos- Same
cfiformat 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-unreaddelete fromreading_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
Released as v0.2.9 on 2026-05-09
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.pysetneeds_review: Trueonupsert_book(wasFalse); the New view filters onneeds_review == True, so previously grabbed books never showed up there - Matches the behavior of disk-scanned imports (
library.pyalready setsneeds_review = Truefor freshly discovered files)
- Both the DB-storage and file-EPUB branches in
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()inxhtml.pymatches 2+ consecutive<center><img src=".../break.png" .../></center>occurrences (with optional whitespace in between) and replaces them with one - Applied in
normalize_wysiwyg_html()(editor save path) and inrouters/grabber.pyon both the previewconverted_xhtmland the per-chaptercontent_htmlproduced during scraping
- New helper
- Docs:
docs/TECHNICAL.mdupdated to cover previously missing changes —POST /api/edit/intro/{filename}and thetitlefield on file-EPUB chapter save; FlareSolverr sidecar andBaseScraper.close();AwesomeDudeScraperuses FlareSolverr;make_epub(include_intro=…)andepub_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
flaresolverrinstack/stack.yml(imageghcr.io/flaresolverr/flaresolverr:latest, internal-only, onnovela-net);novelaservice gainsFLARESOLVERR_URL=http://flaresolverr:8191/v1and adepends_on: flaresolverr - New helpers in
scrapers/base.py:flaresolverr_get(url, timeout_ms=None, session=None)postscmd: request.getand returns aSimpleNamespace(text, url)(drop-in forhttpx.Responseattributes);flaresolverr_session_create()andflaresolverr_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_URLandFLARESOLVERR_TIMEOUT_MSenv vars (defaults: service DNS name and 60000 ms) BaseScrapergained an asyncclose()method (default no-op) for releasing scraper-scoped resources;scrapers/awesomedude.pycreates a FlareSolverr session infetch_book_info, reuses it in allfetch_chaptercalls, and destroys it inclose()routers/grabber.pynow wraps all scraper usage intry/finally: await scraper.close()so sessions are released even on errors- Stack uses
${NOVELA_PORT}and${ADMINER_PORT}(defined instack/novela.envas 8099 / 8098) so host-port values don't diverge between environments
- New service
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): thechapter-title-inputis always shown (the read-onlyheader-chapterlabel is hidden for both storage types), title changes mark the chapter dirty, and the title is sent in bothsaveChapterandsaveAllChapters - Backend (
routers/editor.py):POST /api/edit/chapter/{index}/{filename}for file EPUBs now acceptstitlealongsidecontent; if the title changed it calls new helper_update_epub_navpoint_title(path, href, new_title)which locates the matching NCXnavPointby content src and rewrites its<text>viarewrite_epub_entries
- Frontend (
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_indexvalues up by one and inserts"Book Info"at index 0 (two-step negation to avoid unique-constraint violations) - File EPUBs: writes a new
intro_<hex>.xhtmlviamake_intro_xhtml, adds a manifest item, places theitemrefat the start of the spine, and inserts a navPoint at the top of the NCX with renumberedplayOrder - 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
- New endpoint
- 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.xhtmlviamake_epubas before - DB → EPUB export (
reader.py): no longer synthesisesintro.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_epubgained aninclude_intro: bool = Trueparameter; when false, theintro.xhtmlfile, its manifest item, its spineitemref, and its NCX navPoint are all omitted (remainingplayOrdervalues 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<h1>{title}</h1>; skips empty fields and separates description and source/updated blocks with<hr/> - DB-storage info page: chapter title is
"Book Info"; to preserve the leading<h1>{book title}</h1>in its body, the leading-h-tag stripping inreader.py(get_chapter_htmland the DB→EPUB export) is now skipped whentitle == "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_filesaddsscanned_files INTEGERcolumn tobackup_log _run_backup_internalnow returns(scanned_files, uploaded_count, uploaded_size)_finish_backup_log(...)acceptsscanned_files=;_run_backup_jobpasses the value through/api/backup/statusand/api/backup/historyreturnuploaded_files(oldfiles_count) andscanned_filesbackup.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)
- New migration
- Backup page: live phase indicator under the Run buttons while a backup is running
- New
#run-progressstatus line is filled bypollRunProgress()which polls/api/backup/progressevery 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 atN / Nduring 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 reportsrunning=false
- New
Released as v0.2.4 on 2026-04-21
2026-04-21
- Backup: fix Dropbox
Read timed out. (read timeout=120)error on large uploadsdropbox.Dropbox(...)timeoutraised from120to300inrouters/backup.py(_dbx(), both refresh-token and legacy-token branches)_DROPBOX_UPLOAD_CHUNKreduced from100 * 1024 * 1024(100 MB) to16 * 1024 * 1024(16 MB)_DROPBOX_UPLOAD_THRESHOLDlowered 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
<span class="subheading">(red, bold in reader) - C (chat) — wraps selection in
<span class="chat">(orange in reader) - →| (indent) — wraps selection in
<p style="padding-left: 40px;">(or<div>when the selection contains block elements) - [ ] (comment) — wraps selection in
<div class="novela-comment">(blue left border + subtle background in reader) - Without a selection: the tag is inserted at the cursor with the cursor positioned inside
- All four buttons disabled when no chapter is loaded; enabled state mirrors the existing Break button
- Wrap logic auto-detects block content in the selection:
wrapSpanandinsertIndentuse a<div>wrapper instead of<span>/<p>when the selection contains<p>,<div>,<h*>etc. to keep the HTML valid - Reader CSS extended:
span.subheading,span.chat,p[style*="padding-left"], and.novela-commentare styled inreader.html
- S (subheading) — wraps selection in
2026-04-16 (1)
- Startup: migration logging zichtbaar in Docker logs
logging.basicConfig(level=logging.INFO)toegevoegd aanmain.pymigrations.pylogt per migratie of deze overgeslagen of uitgevoerd wordt (met duur in ms)- Samenvattingsregel bij afsluiting: "all already applied" of "N executed"
Released as v0.2.1 on 2026-04-16
2026-04-15 (4)
- Scraper: fix encoding for Codey's World
- Pages are decoded with
cp1252(Windows-1252) instead of relying onr.textorhtml.parserauto-detection viaiso-8859-1 cp1252correctly maps the 0x80–0x9F byte range:…,',',",",–,—etc. now render correctly instead of producing replacement characters
- Pages are decoded with
- XHTML: normalize non-breaking spaces globally
\xa0(HTML ) is now replaced with a regular space before HTML-escaping in bothelement_to_xhtmlandnormalize_wysiwyg_html- Consecutive spaces resulting from this substitution are collapsed to a single space
- Applies to all scrapers
2026-04-15 (3)
- Editor: chapter add/delete deferred until Save is clicked
- Adding or deleting a chapter no longer triggers an immediate API call
- All structural changes are collected in memory and applied (in the correct order) when the Save button is pressed
- Deletes are applied in reverse server-index order to avoid index shifting errors; new chapters are appended afterwards
- Fix:
savingflag is set at the start of a delete operation to prevent a concurrentsaveChapterfrom flushing pending changes during the async gap inloadChapter
- Refactor: fix unclosed file handles in
epub.pymake_epubandwrite_epub_filenow usePath.read_text()/Path.read_bytes()instead of bareopen()calls
- Refactor: eliminate temp file in
pdf_cover_thumb- Cover thumbnail is now generated fully in-memory via
Image.frombytes()from the PyMuPDF pixmap — removes the race condition when multiple requests hit the same PDF simultaneously
- Cover thumbnail is now generated fully in-memory via
- Refactor: harden
security.py- Hardcoded fallback encryption key removed; raises
RuntimeErrorwith a clear message when neitherNOVELA_MASTER_KEYnorPOSTGRES_PASSWORDis set - Fernet instance cached with
@functools.lru_cache(maxsize=1)— key derivation runs only once per process
- Hardcoded fallback encryption key removed; raises
- Add
GET /healthendpoint- Runs
SELECT 1against the database and returns{"ok": true/false}
- Runs
- Performance: cap in-memory job dicts at 50 entries
JOBSingrabber.pyandBACKUP_TASKS/BACKUP_PROGRESSinbackup.pyare trimmed (oldest-first) whenever a new entry is added
- Performance: improve ETag accuracy for the library API
- ETag now includes
MAX(updated_at)fromreading_progressandMAX(id)frombook_tags, so tag changes and progress updates correctly invalidate the client cache
- ETag now includes
- Performance: cache CBR/CBZ page list
cbr_page_listis cached per(path, mtime)vialru_cache(maxsize=64)— avoids opening the archive twice per page request
- Refactor: normalize transaction handling in
builder.py- All
conn.commit()calls replaced withwith conn:context manager, consistent with the rest of the codebase
- All
Released as v0.2.0 on 2026-04-15
2026-04-15 (2)
- Editor: fix chapter add failing with UniqueViolation on DB-stored books
- PostgreSQL checks the unique constraint on
(filename, chapter_index)per row duringUPDATE, so incrementing consecutive indices in a single statement (e.g. 1→2 while 2 exists) raised aUniqueViolation - Fixed by using a two-step approach: first shift affected rows to temporary negative values, then to their final positive values
- PostgreSQL checks the unique constraint on
- Refactor: eliminate duplicated EPUB helper functions across
reader.py,editor.py,common.py- New
epub_utils.pywith sharedfind_opf_path,norm_href,epub_spine,make_new_chapter_xhtml,rewrite_epub_entries - Fixes the double-escaped
\\\\s*regex in the old_epub_spineOPF path lookup (was silently falling back to directory scan) rewrite_epub_entriescombines crash-safe.tmp.epubwrite withZIP_STOREDfor themimetypeentry (EPUB spec requirement)- All private
_epub_spine,_norm_href,_find_opf_path,_make_new_chapter_xhtml,_rewrite_epub_entriescopies removed fromreader.py,editor.py,common.py
- New
- Migrations: run each migration only once via
schema_migrationstracking table- Eliminates heavy
rebuild_chapter_tsv_with_titleUPDATE running on every container restart - Reduces startup from 37 separate DB connections to 1
- Eliminates heavy
2026-04-15 (1)
- Reader: font size control in reading settings
- New "Font size" slider (80–150%, default 105%) in the settings drawer, between "Content width" and "Text colour"
- Applies via CSS custom property
--reader-font-sizeon#chapter-content - Persisted per-device in
localStorageasreader-font-size— iPad and desktop each remember their own preference
2026-04-13 (1)
- Edit metadata: comma-separated tag input fix
PillInput._addinbook.jsnow splits the incoming value on commas before adding — each trimmed, non-empty, non-duplicate part is pushed individually- Applies to genres, subgenres and plain tags; pasting e.g.
Fiction, Thriller, Adventureadds 3 separate pills instead of 1
Released as v0.1.11 on 2026-04-13
2026-04-12 (2)
- Series navigation in the reader
- New
GET /api/series-nav/{filename}endpoint: returns{prev, next}with{filename, title, index, suffix}for adjacent books in the same series, ordered byseries_index ASC, series_suffix ASC; returns{prev: null, next: null}for books without a series - Reader header now shows prev/next series buttons (skip-to-start/skip-to-end icons); hidden for books without a series, visible once
loadSeriesNav()resolves - Hovering a series button shows a tooltip:
#<index><suffix> <title>(e.g.#4 Batman: Year One) markRead()in the reader redirects to/library/read/{next.filename}when a next volume exists, so reading continues without leaving the reader; falls back to the book detail page when the series is complete or the book has no series
- New
2026-04-12 (1)
- Comics: series_volume support for annual series (issue numbers restart each year)
- New
series_volume VARCHAR(20)column onlibrary(migrationmigrate_series_volume); default'' - Stored in EPUB OPF as
<meta name="novela:series_volume" content="…"/>; read back byscan_epub upsert_bookinserts/preservesseries_volumewith the same COALESCE strategy asseries_suffixlist_library_jsonORDER BY now:publisher → author → series → series_volume → series_index → titlePATCH /library/book/{filename}readsseries_volumefrom request body; persists to DB and OPF for both file-based and DB-stored books- Book detail page: displays
(year)after series name whenseries_volumeis set (e.g.Donald Duck (1982) [15]); edit panel has a new "Year/Volume" input field book.js: "Auto" button next to the Title field generatesSeries (Year/Vol) #Numberfrom the current series fields
- New
- Bulk importer:
series_volumesupport- New
%series_volume%placeholder token (orange) for filename pattern parsing - New "Year/Vol." shared metadata field (applies to all files; overridden by per-row value or pattern)
- Preview table has a new "Yr/Vol" column
- New "Auto-generate titles from series info" checkbox: when enabled, rows without a parsed title get title
Series (Year/Vol) #Number - Skip checkbox now always visible for every row (previously only shown when duplicates were detected); any file can be manually excluded before import; skipped rows are dimmed; stats bar shows "X skipped"
POST /api/bulk-check-duplicatesextended: now also checks(series, series_index, author)as fallback — detects duplicates even when the title format has changed; items must includeseriesfield
- New
- Library front-end: sorting within a series now respects
series_volumebeforeseries_indexgroupBySeries,renderAuthorDetail,renderPublisherDetailall sort byseries_volume → series_index → series_suffixgetSeriesSlotsrefactored: when any book in the series hasseries_volumeset, gap-detection runs per volume (year) independently — prevents#5 (1982)and#5 (1983)from colliding in the same slot- Slot index label shows
(year) #indexfor annual series (e.g.(1982) #5); unchanged for regular series
Released as v0.1.10 on 2026-04-12
2026-04-08 (13)
- Library: archive series in one action
- "Archive series" / "Unarchive series" button in the series detail view (
#series/{name}) - New endpoint
POST /library/archive-series— setsarchivedfor all books in the series via a single SQL UPDATE; body:{"series": "…", "archive": true|false}; returns{ok, archived, count} - Button label reflects current state: "Archive series" when any book is active, "Unarchive series" when all are archived
- After the call,
allBooksis updated in place and sidebar counters are recalculated without a full page reload
- "Archive series" / "Unarchive series" button in the series detail view (
2026-04-08 (12)
- TedLouisScraper: title extraction fix
<h2 class="story-page-title">also contains a "Back" button (<a class="btn">) and the author byline (<span class="story-author-by-line">)- Fix: only direct
NavigableStringchildren 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) fortedlouis.com- Matches all
tedlouis.comURLs; 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
<h2 class="story-page-title"> - Author from
<span class="story-author-by-line"> <a> - Status from
<span class="story-status-text">(strips "Status: " prefix) - Updated date from
<span class="story-last-updated">→YYYY-MM-DD - Chapters from all
<ul class="story-index-list">elements (three columns); relative?t=TOKENlinks resolved to absolute URLs fetch_chapter(): content from<div id="chapter">; removes story title, chapter title and copyright blocks
- Matches all
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.pngso EPUB export uses the same image app_settingsextended withbreak_image_sha256andbreak_image_extcolumns (migrationmigrate_app_settings_break_image)- New endpoints:
POST /api/app-settings/break-image;GET /api/app-settingsnow also returnsbreak_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 (
<center><img src="../Images/break.png">) contain a relative EPUB path that does not exist in the DB context - Fix:
storage_modedetermined earlier in_run_scrape; for DB mode/static/break.pngis passed asbreak_img_pathtoelement_to_xhtml, for EPUB mode../Images/break.pngis kept - Additional fix: when a break image is detected in the image loop, the parent (
<center>) is replaced with<hr>instead of decomposing the<img>; this allowselement_to_xhtmlto correctly detect the break after the image loop
- Break images (
2026-04-08 (8)
- New scraper:
IomfatsScraper(scrapers/iomfats.py) foriomfats.org- Matches all
iomfats.orgURLs; 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
<h3>= book title; chapters directly in<ul> - Multi-part series: outer
<h3>= series name; nested<li><h3>= book title per part; chapters in sub-<ul>
- Single story: outer
- Series index from folder name:
*-part{N}or*-{N}→series_index_hint - Publication status from
<p><small>[...]</small></p>after book title fetch_chapter(): content via<div id="content">; removes<h2>/<h3>headings, chapternav divs, footer elements
- Matches all
2026-04-08 (7)
- New scraper:
CodeysWorldScraper(scrapers/codeysworld.py) forcodeysworld.org- Matches all
codeysworld.orgURLs; no login required - Title from
<h1>; author from<h2>("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/.htmlfiles in the same directory (audio/image links skipped) fetch_chapter(): removes all<h1>/<h2>headings, navigation links ("Back to …"), audio links (.mp3), mailto links
- Matches all
- Nifty scraper: category/subcategory moved from
genres/subgenrestotags
2026-04-08 (6)
- NiftyNewScraper: chapter extraction made more robust
fetch_chapter()now first tries the standard chapter HTML ({url}) and reads<article><p>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:
<!-- NIFTY_HIDDEN_LEAD: ... --><!-- NIFTY_HIDDEN_TAIL: ... -->
- Detection also works when text contains inline HTML (e.g.
<a>or<svg>in donate links)
- Lead/tail detection added for common blocks (e.g.
2026-04-08 (5)
- NiftyNewScraper: chapter content fix
new.nifty.orguses Next.js RSC — chapter content is not present in the static HTML but in the RSC payload (React component tree)fetch_chapter()now fetches{url}?_rsc=1instead of the regular HTML page- RSC parser added:
_parse_rsc_paragraphs(),_rsc_find_paragraphs(),_rsc_text()— parse the RSC stream line by line (format:{hex_id}:{json}), recursively search for["$","p",null,{...}]nodes and extract text fromchildren
2026-04-08 (4)
- NiftyNewScraper added (
scrapers/nifty_new.py) fornew.nifty.org- Matches all
new.nifty.orgURLs; no login required _to_index_url(): strips trailing/N(chapter number) so both index and chapter URLs can be used as entry pointfetch_book_info(): title from<h1>, author from<strong itemprop="name">in author link, dates from<time itemprop="datePublished/dateModified">, tags from all<ul aria-label="Tags">containers (category links and generated tags, deduplicated), description from<meta name="description">, chapter list via<a>links matching/stories/{slug}/N(RSC stream regex as fallback)fetch_chapter(): title from JSON-LD@type: "Chapter", content from<article>; no email header stripping, no line joining, no boilerplate detectionNiftyScraper.matches()updated: excludesnew.nifty.org("nifty.org" in url and "new.nifty.org" not in url)NiftyNewScraperregistered beforeNiftyScraperinscrapers/__init__.py
- Matches all
2026-04-08 (3)
- Settings: develop mode added
- New
app_settingstable (single row,id = 1) withdevelop_modeboolean; created viamigrate_create_app_settings() shared_templates.py: sharedJinja2Templatesinstance for all routers;develop_mode()registered as Jinja2 global so all templates can access it without explicit context injection- All 11 routers now import
templatesfromshared_templatesinstead of each creating their own instance - New endpoints in
routers/settings.py:GET /api/app-settingsandPATCH /api/app-settings - Diagonal DEVELOP banner in the top-left of every page (CSS in
static/sidebar.css, HTML intemplates/_sidebar.html); only visible when develop mode is enabled - All 17 HTML templates:
<title>shows Novela Develop — … when develop mode is active - Settings page: new card with checkbox toggle; page reloads after saving so banner and title apply immediately
- New
2026-04-08 (2)
- Nifty scraper: fix
_strip_email_headersnow tolerates blank lines between header fields- Some Nifty pages place
Subject:after a blank line (Date:\nFrom:\n\nSubject:\n) — the previous implementation stopped at the first blank line causingSubject:to appear as a paragraph in chapter text - Fix: on a blank line, look ahead to check whether a header field still follows; if so, skip the blank line(s) and continue stripping
- Some Nifty pages place
2026-04-08 (1)
- Nifty scraper added (
scrapers/nifty.py)- Matches all
nifty.orgURLs; no login required fetch_book_info: accepts index or chapter URL; normalises to index; title from URL slug, author and publication date from email headers of chapter 1,updated_datefrom email headers of the last chapter; genres/subgenres from URL path (/nifty/{category}/{subcategory}/…)- Boilerplate detection: compares first paragraphs of chapters 1 and 2 after header stripping; matching paragraphs are skipped in all chapters (
preamble_countper chapter dict) fetch_chapter: retrieves<pre>content (fallback: body); subject header stored as invisible HTML comment<!-- Subject: … -->at the top of chapter content; email headers stripped; hard line breaks within paragraphs joined into a single line; scene breaks (***,---, etc.) →<hr/>- Date parsing via
email.utils.parsedate→YYYY-MM-DD
- Matches all
xhtml.py:element_to_xhtmlnow handlesbs4.Commentobjects as XML comments (<!-- … -->);--in the body is sanitised to- -(illegal in XML comments)
2026-04-06 (3)
- Book detail: rating moved from clickable stars to dropdown in the Edit metadata panel
- Stars on the detail page are now purely visual (no longer clickable)
- New
<select id="ed-rating">field in the edit panel, directly below Status openEdit()populates the select with the current rating;saveEdit()callsPOST /library/rating/…if the value has changedrateBook()function removed frombook.js- Overview pages unchanged
2026-04-06 (2)
- Library: cover upload now also supported for DB-stored books
POST /library/cover/{filename}previously returned an error for DB books ("File not found") because no physical file exists- Fix: DB books are now detected via
is_db_filename; cover is stored directly inlibrary_cover_cacheandhas_cover = TRUEset in thelibrarytable —add_cover_to_epubis not called
2026-04-06 (1)
- Search: filter for unread novels / unread shorts
- New
filterparameter onGET /api/search?q=…&filter=all|unread_novels|unread_shorts(default:all) unread_novels: restricts to books with no reading sessions/progress and noShortstagunread_shorts: restricts to books with no reading sessions/progress and with aShortstag- UI: second toggle row (All / Unread novels / Unread shorts) below the Phrase/All words toggle
- Filter persisted in URL (
?filter=…) and restored on page load
- New
2026-04-05 (4)
- Backup: large files (> 148 MB) now uploaded via Dropbox upload session in 100 MB chunks
_dropbox_upload_bytes: files ≤ 148 MB go viafiles_upload(unchanged); larger files viaupload_session_start→upload_session_append_v2→upload_session_finish- Fixes
ApiError: UploadError('payload_too_large')
2026-04-05 (3)
- Filenames: spaces now replaced with underscores when saving new files
clean_segment(common.py) and_clean_segment(reader.py):\s+→_instead of space- Series separator changed from
-to_-_(applies to EPUB, CBR, CBZ and DB filenames) - Existing files are not renamed
2026-04-05 (2)
- Search: removed result limit; added Phrase / All words mode toggle
LIMIT 30removed — all matching chapters are returned- New
modeparameter onGET /api/search?q=…&mode=phrase|words:phrase(default) requires words in order (phraseto_tsquery);wordsrequires all words present in any order (plainto_tsquery) - Toggle in the UI (Phrase / All words) above the results; mode is included in the URL
2026-04-05 (1)
- Export EPUB: double chapter titles fixed — same heading-stripping logic as the reader now applied before passing content to
make_chapter_xhtml - Library: authors and publishers with only archived books now remain visible in the Authors and Publishers list views
renderAuthorsViewandrenderPublishersViewswitched fromactiveBooks()toallBooks— consistent withrenderAuthorDetailandrenderPublisherDetail
2026-04-04 (2)
- Reader: fixed double chapter titles for books where the heading is wrapped in a
<section>element (pandoc-style)- Previous regex only stripped
<h1>–<h4>at the very start of content; pandoc-converted EPUBs wrap headings as<section …><h1>…</h1>— heading was not at position 0 so the regex didn't fire - Added a second
re.subpass that removes the first<h1>–<h4>found directly after an opening<section>or<div>tag, preserving the wrapper element - Same stripping applied in the DB→EPUB export (
export_epub) before passing content tomake_chapter_xhtml
- Previous regex only stripped
- Search: switched from
plainto_tsquerytophraseto_tsqueryin the FTS WHERE clauseplainto_tsqueryANDs all words but treats them as independent terms (any order, any distance) — multi-word queries like "4 years later" matched chapters where the words appeared far apartphraseto_tsqueryrequires all words to appear in sequence;ts_rankandts_headlinestill useplainto_tsqueryfor correct scoring and highlighting
2026-04-04 (1)
- Reader: fixed double chapter titles in DB-stored books
- Chapter endpoint (
GET /library/chapter/{index}/{filename}) now strips all leading<h1>–<h4>tags from stored content before prepending its own<h2 class="chapter-title">— books scraped before front-matter stripping was added to the scraper showed the title (and chapter heading) twice
- Chapter endpoint (
- Library: archived books now visible in author and publisher detail views, with an indicator badge on the cover
renderAuthorDetailandrenderPublisherDetailreverted to useallBooks(including archived) so archived books remain accessible from the author/publisher pages- New
.badge-archivedoverlay (bottom-left of cover): dark circle with archive icon, consistent with existingbadge-statusandread-pilloverlays; added to all three card-rendering code paths
2026-04-03 (3)
- DB chapter editor: Monaco-based editor now supports DB-stored books
GET /library/editor/{filename}handlesdb/…filenames;is_dbflag passed to templateGET /api/edit/chapter/{index}/{filename}andPOST …: DB branches query/updatebook_chaptersdirectly; save callsupsert_chapter(updatescontent_tsvtoo)POST /api/edit/chapter/add/{filename}andDELETE …: DB branches insert/delete withchapter_indexshift viaUPDATE … SET chapter_index = chapter_index ± 1- Title editing: header chapter-name replaced with a text input for DB books;
pendingTitlesmap preserves unsaved titles across chapter switches (parallel topendingContent); title-only dirty chapters correctly saved in Save All insertBreak: scene-break image path is/static/break.pngfor DB books (vs../Images/break.pngfor EPUB)- Fix:
editor.focus()called after content load so Monaco receives keyboard focus immediately - Fix:
header-chapter"Loading…" text suppressed for DB books where that element is hidden book.html: "Edit chapters" button shown forstorage_type = 'db'books
- Search: chapter titles now included in FTS
upsert_chapterprepends title to the plain-text input forto_tsvector:title + " " + stripped_htmlGET /api/search: addedOR LOWER(bc.title) LIKE LOWER('%…%')fallback for chapters whose title matches but content doesn't- Startup migration
migrate_rebuild_chapter_tsv_with_title()rebuilds existingcontent_tsvvalues to include titles
- Grabber: added DB/EPUB storage toggle on the Convert page
- UI toggle above Convert button ("Save as: DB | EPUB file");
storageModeJS variable sent in POST body POST /convert: readsstorage_modefrom body; stored in job as'db'or'epub'_run_scrape: EPUB path builds chapters viamake_chapter_xhtml, callsmake_epub, writes file, callsupsert_book(storage_type='file'); DB path unchangeddoneSSE event includesstorage_type;conversion.jsupdates the download button label/action accordingly
- UI toggle above Convert button ("Save as: DB | EPUB file");
- EPUB → DB conversion: fixed double chapter title
_epub_body_innerstrips the first<h1>/<h2>/<h3>heading from each chapter body before storing; the editor prepends its own heading, so storing the EPUB heading too caused it to appear twice- Fix for
NavigableStringcrash:getattr(child, "name", None) is Noneused instead ofhasattr(child, "name")—NavigableStringhasname = Nonebut nodecompose()method
- Sidebar: Search link styling fixed
- Stray
<li>Search</li>moved inside the Library<ul class="sidebar-nav">(was outside, causing incorrect HTML structure) sidebar.css: addeda:visited { color: var(--text-dim) }anda.active:visited { color: var(--accent) }to prevent the browser's default purple visited color
- Stray
2026-04-03 (2)
- DB-stored books (Fase 4–6): EPUB→DB conversion, DB→EPUB export, full-text search
- Fase 4 — EPUB-to-DB conversion:
POST /api/library/convert-to-db/{filename}converts an existing on-disk EPUB to a DB-stored book; extracts chapters via_epub_body_inner(rewrites img src to imagestore URLs), migrates all child rows (book_tags, reading_progress, reading_sessions, bookmarks, library_cover_cache) to the newdb/…filename using INSERT→UPDATE→DELETE to respect FK constraints, then deletes the EPUB file - Fase 5 — DB→EPUB export:
GET /api/library/export-epub/{filename}builds and streams an EPUB from DB content;_rewrite_db_images_for_epubrewrites/library/db-images/…URLs back toOEBPS/Images/…paths, deduplicating by sha256;Content-Disposition: attachmentresponse - Fase 6 — Full-text search: new
routers/search.pywithGET /search(page) andGET /api/search?q=…(FTS overbook_chapters.content_tsvviaplainto_tsquery('simple', q),ts_headlinefor snippets,ts_rankfor ordering, LIMIT 30, excludes archived); newtemplates/search.htmlwith highlight (<mark>), "Read here" link (?bm_ch=N&bm_scroll=0), and "Book detail" link; Search entry added to sidebar book.html: DB books show "Export EPUB" instead of "Download"; "Edit EPUB" and "Convert to DB" buttons only shown for.epubfiles; delete modal text differs for DB vs file booksPATCH /library/book/{filename}: DB book branch added — skips file move, recomputes syntheticdb/…filename viamake_rel_path, applies same FK-safe rename pattern, updatesbook_chaptersandbookmarksin addition to standard child tables
- Fase 4 — EPUB-to-DB conversion:
2026-04-03 (1)
- DB-stored books (Fase 1–3): grabber now stores scraped books in PostgreSQL instead of EPUB files on disk
- New
book_chapterstable:filename FK, chapter_index, title, content TEXT, content_tsv TSVECTOR; GIN index oncontent_tsvfor future FTS - New
book_imagestable:sha256 PK, ext, media_type, size_bytes; content-addressed imagestore atlibrary/images/{sha2}/{sha256}{ext} - New
storage_type VARCHAR(10) DEFAULT 'file'column onlibrary; DB-stored books use'db' - New utilities in
common.py:is_db_filename,write_image_file,store_db_image,html_to_plain,upsert_chapter,ensure_unique_db_filename;make_rel_pathnow handlesmedia_type="db"→ syntheticdb/{pub}/{auth}/...filename upsert_bookandlist_library_jsonupdated to includestorage_type- Grabber:
_run_scrapestores chapters inbook_chapters, chapter images in imagestore (absolute/library/db-images/URLs embedded in HTML), cover inlibrary_cover_cache; no EPUB file written - New
GET /library/db-images/{path:path}endpoint serves imagestore files - Reader:
GET /library/chapters/andGET /library/chapter/have DB branches forstorage_type='db'books (querybook_chaptersdirectly) - Reader page (
/library/read/), book detail page, mark-read, and rating endpoints all handle DB filenames (no file existence required) - Cover endpoints (
/library/cover/,/library/cover-cached/) serve DB books fromlibrary_cover_cache
- New
2026-04-02 (1)
- Added Restore functionality to the Backup page
- New
GET /api/backup/snapshotsendpoint: lists available Dropbox snapshots (name + date parsed from filename, no downloads needed) - New
GET /api/backup/snapshots/{snapshot_name}/filesendpoint: loads a snapshot from Dropbox and returns all files with path, size, sha256, and whether the file currently exists locally - New
POST /api/backup/restoreendpoint: downloads file objects from Dropbox, writes to disk, and re-indexes viascan_media+upsert_book; returns per-file result with errors - New "Restore" card on the backup page: snapshot dropdown (auto-loaded on page open), file list with filter/search, per-file "Restore" button, multi-select + "Restore selected", on-disk indicator, inline status feedback
- After restore, the file list refreshes to reflect updated on-disk state
- New
2026-03-29 (10)
- Duplicates: fixed
updateCountscrashing with a TypeError (g.books.length→g.length); the crash preventedrenderGridfrom running, so the duplicates view never rendered and the counter was stale
2026-03-29 (9)
- Duplicates: volume-aware duplicate detection — a book is only a duplicate when title + author + volume all match
_duplicateGroupsinlibrary.js(Duplicates view): key now includesseries_indexwhen > 0, so different volumes of the same series are no longer grouped as duplicatespreloadingrabber.py(Grabber): when the scraper returns aseries_index_hint, the DB lookup only flags a match when title + author + volume all match; falls back to title + author when no volume is known — same logic as the bulk importer
2026-03-29 (8)
- Shared code: eliminated all remaining duplication across templates and JS files
- CSS custom properties: extracted single
:root { }block intostatic/theme.css; removed duplicate inline:rootfrom all 15 templates and fromlibrary.css,book.css—editor.csskeeps editor-specific vars (--danger,--header-h,--panel-w),reader.htmlkeeps page-specific vars (--header-h,--footer-h,--content-w),backup.htmlkeeps (--ok,--warn,--err) - Cover helpers: moved
strHash,COVER_PALETTES,makePlaceholderCover,wrapText,truncatefromlibrary.jsandbook.jsintobooks.js; removed fromhome.htmlandfollowing.html - HTML escape:
esc()added tobooks.js; removed fromlibrary.js,editor.js, and all 8 templates that defined it inline - SSE/EventSource: extracted shared
connectConversionStream(job_id)andaddLog()into newstatic/conversion.js; bothindex.htmlandgrabber.htmlnow call the shared function (removed ~70 duplicate lines)
- CSS custom properties: extracted single
2026-03-29 (7)
- Search: extracted shared book helpers and search logic into
static/books.js_filenameBase,bookTitle,bookAuthor,tagValuesByType,bookGenres,bookSubgenres,bookPlainTags,filterBooks,setupSearchInputmoved fromlibrary.jsandhome.htmlto a single shared file- Both pages now use identical search behaviour: Enter to search, × to clear
- Both pages now search across title, author, publisher, genre, sub-genre, and tag
2026-03-29 (6)
- Search: changed from search-as-you-type (250 ms debounce) to Enter-to-search on both Library and Home — prevents iPad keyboard from locking up on large collections
2026-03-29 (5)
- Accent colour updated to match logo orange (
#ffa20e); secondary accent updated to#ffb840- Applied across all CSS files, templates, and inline styles
2026-03-29 (4)
- Branding: added logo, favicon, and Apple touch icon to all pages
- Static assets:
logo.png(sidebar),favicon.ico(16×16),favicon-32.png(32×32),favicon-256.png(256×256),apple-touch-icon.png(180×180 with#0f0e0cbackground) - Favicon
<link>tags added to all 15 templates - Sidebar: image logo (
logo.png) placed next to the existing "Novela" wordmark using flexbox apple-touch-icon.pnguses dark#0f0e0cbackground — renders as a native-looking iOS home screen icon
- Static assets:
2026-03-29 (3)
- Dockerfile: replaced
unrar-freewith proprietaryunrar(RARLAB v6.2.6) from Debian non-free — fixes "Failed to read enough data" errors on RAR archives using newer compression methods
2026-03-29 (2)
- CBR reader: detect archive format via magic bytes instead of file extension —
.cbrfiles that are actually ZIP or 7-zip archives now open correctly; addedpy7zrdependency for 7-zip support
2026-03-29 (1)
- Bulk Import: duplicate check now volume-aware — books with the same title+author but a different volume number (e.g. recoloured reprints) are no longer flagged as duplicates;
volumeis included in the API call and matched againstseries_indexin the DB
2026-03-28 (11)
- Bulk Import: duplicate detection against existing library
- New
POST /api/bulk-check-duplicatesendpoint: case-insensitive title+author+volume match, single SQL query with OR conditions for all pairs - Duplicate rows highlighted in red in the preview table; a skip checkbox appears per duplicate row
- Stats bar shows "X duplicates · Skip all · Import all" action buttons
- Duplicate rows are skipped by default; user can toggle individual rows or use bulk actions
startImport()filters out skipped rows before batching; skipped files appear in the result summary as "Duplicate – skipped"- If all rows are skipped (nothing to import), result shown immediately without sending any request
- New
2026-03-28 (10)
- After "Remove from New" succeeds, the library is now reloaded from the server (
loadLibrary()) so the New list updates immediately without a manual refresh
2026-03-28 (9)
- Bulk delete is now batched: new
POST /library/bulk-deleteendpoint accepts a JSON list of filenames, deletes files and removes DB rows in one query per batch; JS sends 20 files per batch with a progress bar in the confirmation dialog
2026-03-28 (8)
- Disk usage warning in sidebar:
GET /api/diskreturns partition usage for the library directory; sidebar polls every 60 s and shows a warning bar (amber ≥ 85% or < 2 GB free, red ≥ 95% or < 500 MB free) above the backup status bar
2026-03-28 (7)
- Fixed
mark-reviewedJS: UI (allBooks update + renderGrid) now only runs after confirmed server success;catchno longer fires a false error after a successful response where renderGrid throws
2026-03-28 (6)
- Bulk Import: rewrote pattern editor from token-pills to free-text
%placeholder%syntax- Pattern input: free-text field; placeholders:
%series%%volume%%title%%year%%month%%day%%author%%publisher%%ignore% - Colored chip row: click to insert at cursor position; drag onto input also supported
- Colored live preview below the pattern input; regex-based parser replaces delimiter+token logic
- Shared metadata now wins over filename-parsed values (previously filename won)
- All UI text translated from Dutch to English
- Pattern input: free-text field; placeholders:
2026-03-28 (5)
- Status badge (top-right cover) and want-to-read star (top-left cover) now use dark fill
rgba(15,14,12,0.82)+box-shadow: 0 0 0 2px #0f0e0cring — always readable regardless of cover colour
2026-03-28 (4)
- Added
Temporary Holdstatus; renamedHiatus→Long-Term Hold- Startup migration
migrate_rename_hiatus()converts existing DB records automatically - Status colours: Complete=green, Ongoing=blue, Temporary Hold=amber, Long-Term Hold=orange
statusBadgeHtml()helper inlibrary.jsreplaces three identical inline badge blocks- Grabber:
Temporary-Hold(gayauthors.org) now maps toTemporary Hold;Long-Term Holdpasses through unchanged - Status dropdowns updated in Book Detail and Bulk Import
- Startup migration
2026-03-28 (3)
- Added Bulk Import page (
/bulk-import) for batch importing CBR/CBZ/EPUB/PDF files with filename-based metadata parsing- Configurable token pattern: Serie, Volume, Titel, Jaar, Auteur, Uitgever, Negeer (in any order)
- Configurable delimiter (default
-); year token right-anchored when last so title absorbs overflow segments - Live test-parse box: type any filename and see how it parses before selecting files
- Shared metadata card: author, publisher, status, genres, tags applied to all files (overridden by pattern tokens or manual edits)
- Preview table: all parsed fields editable via contenteditable cells; warning indicator for rows with fewer segments than tokens; sorted by filename
- Batch upload in groups of 5 with progress bar (N / total files processed)
- Result summary with list of skipped files and reasons
- New
routers/bulk_import.py:GET /bulk-import,POST /library/bulk-import - Sidebar link under Tools between Book Builder and Credentials
This file tracks changes on the develop line.
changelog.md can later be used for release summaries.
2026-03-28 (2)
- Performance: library page now loads instantly for large collections (1000+ books)
IntersectionObserverdefers both cover image loading and placeholder canvas drawing until cards enter the viewport — eliminates hundreds of upfront canvas ops that blocked the initial renderETagcaching on/library/list: server returns304 Not Modifiedwhen nothing changed, client skips JSON parse and re-download- Single DOM pass in
renderBooksGrid,renderDuplicatesView,renderSeriesDetail: canvas and img set up viacard.querySelectorimmediately afterinnerHTML, removing a second iteration withdocument.getElementByIdper card book_tagsjoined viajson_aggin the mainlist_library_json()query, eliminating a separateSELECT * FROM book_tagsquery and Python merge looploadLibrarynow shows an error message instead of staying stuck on "Loading…" when the fetch or render fails
2026-03-28 (1)
- Added Following page (
/following): track external author URLs outside Library and Tools- New
authorstable:name(unique),url,created_at,updated_at - New
routers/following.py:GET /followingpage,GET /api/following(all authors + URL + book count + last added),POST /api/following/{name}(set/clear URL) - Sidebar: new Following section between Library and Tools; counter shows number of followed authors
- Following page: two tabs — Following (authors with URL) and All Authors; inline URL editing with Enter/Escape keyboard support; Visit button opens external URL in a new tab; author name links to library author view
- New
- Added Incomplete view to Library (
#incomplete): shows all non-archived books wherepublication_status ≠ Complete; sidebar counter included; entry placed after New in the Library section
2026-03-27 (1)
- Convert page: duplicate warning shown after loading metadata when a book with the same title+author already exists in the library; warning includes a link to the existing book; user can still proceed with conversion
- Library: added Duplicates section to sidebar (between Rated and Statistics); counter shows total number of books that are part of a duplicate group (same title+author, case-insensitive); Duplicates view groups books by title+author with a subheading per group
- Fixed Duplicates view not loading covers: card renderer now uses the same canvas + two-pass img/
makePlaceholderCoverpattern asrenderBooksGrid
2026-03-26 (2)
- Fixed Book Builder page showing white background:
library.cssadded tobuilder.htmlto load:rootCSS variables and darkbodybackground; all CSS variable references inbuilder.cssaligned with library theme names (--text,--surface,--surface2,--text-dim,--border,--accent,--sidebar)
2026-03-26 (1)
- Added Book Builder: create EPUB books manually from scratch via a WYSIWYG editor
- New
builder_draftstable (id UUID,title,author,publisher,source_url,chapters JSONB) build_epub()inepub.py: builds a standards-compliant EPUB 2.0 ZIP from title/author/publisher/chapters; embeds inline CSS andbreak.pngif presentnormalize_wysiwyg_html()inxhtml.py: converts contenteditable HTML to EPUB-safe XHTML; handles scene-breaks,<blockquote class="author-note">, inline formatting, and<hr>→ break imagerouters/builder.py: draft CRUD, chapter CRUD (GET/POST/PUT/DELETE), normalize preview endpoint, publish endpoint (normalizes all chapters → builds EPUB → writes tolibrary/epub/→ upserts to DB → deletes draft → redirects to book detail)templates/builder.html+static/builder.js+static/builder.css: index page (new draft form + draft list) and editor (chapter panel, contenteditable pane, toolbar with bold/italic/underline/blockquote/author-note/scene-break/normalize, autosave every 30 s, Ctrl+S, publish button)- Book Builder link added to sidebar Tools section (between Convert and Credentials)
.author-noteblockquote style added tostatic/epub-style.css
- New
2026-03-25 (20)
- Added Rated section to library sidebar: shows all non-archived books with
rating > 0, sorted by rating descending then title alphabetically; badge displays total count; navigable via#ratedURL hash
2026-03-25 (19)
- Fixed series index 0 not displaying in series slot view, grid cards, list volume column, and book detail:
- Series slot labels now show
#0for all slots when the series has at least one positively-indexed sibling (consistent label height across all cards) - Grid card and list volume column use the same cross-book heuristic (
indexedSeriesSet) to show[0]where appropriate - Book detail page queries whether any sibling in the same series has
series_index > 0(series_is_indexed) and shows[0]accordingly
- Series slot labels now show
2026-03-25 (18)
- Added autocomplete for Author, Publisher, and Series in the book edit panel: typing shows a filtered dropdown of existing values from the library; selecting a value fills the field (same dropdown styling as genres/tags)
- Status field now defaults to "Complete" when opening the edit panel for a book that has no status set yet
2026-03-25 (17)
- Fixed CBR reader showing only first page:
cbr_page_countwas missing from thecbrimport inreader.py;/api/cbr/info/was returning an error, causingpage_countto fall back to 1 and the Next button to remain disabled
2026-03-25 (16)
- Fixed CBR/CBZ reader stuck on loading: added
/api/cbr/info/{filename}endpoint returning{page_count}, and added a CBR/CBZ branch inreader.htmlthat mirrors the PDF paged reader (page images served via/library/cbr/{filename}/{page})
2026-03-25 (15)
- Added series volume 0 and letter suffix support (e.g. "21a", "21b"):
- New
series_suffix VARCHAR(10)column added tolibrarytable viamigrate_series_suffix series_indexlower bound changed from 1 to 0 throughout (index 0 = special/prequel edition)- Volume field in the book editor changed from
type="number"totype="text"— accepts "0", "1", "21a", etc. - Server parses the combined volume string into
series_index(INTEGER) +series_suffix(VARCHAR) viaparse_volume_str - File naming includes suffix:
021a - Title.epub novela:series_suffixmeta tag written to/read from EPUB OPF for persistence across rescans- Series detail view: books sorted by
(series_index, series_suffix); slot labels show "21a"; index 0 shown as slot when any sibling has index > 0 - Grid cards, list view Volume column, author/publisher sort all include suffix
- New
2026-03-25 (14)
- Added multi-select and bulk delete to
All booksList view:- Checkbox column added to the list table (List mode only; Grid mode unchanged)
- Select all / Clear all / Clear selection controls in the controls bar
Shift+clickrange selection on checkboxesDelete selectedbutton (red, disabled when nothing is selected) triggers a confirmation dialog; confirmed deletions remove files from disk and database, then reload the library
2026-03-25 (13)
- Fixed EPUB cover replacement reverting to original after upload:
GET /library/cover/{filename}for EPUBs now checks the DB cover cache first (populated on upload) before falling back to extracting from the EPUB file; the cache is also warmed on cache-miss so subsequent requests are fast.add_cover_to_epubfully rewritten: locates the OPF viaMETA-INF/container.xml, finds the existing cover image in the OPF manifest, removes it from the ZIP, and writes the new cover in the same directory — works for any EPUB directory structure (OEBPS/,EPUB/, etc.).
2026-03-25 (12)
- Fixed chapter images not loading in EPUBs that use a non-standard root directory (e.g.
EPUB/instead ofOEBPS/): image paths are now passed as full ZIP paths instead of stripping the root segment, and the image endpoint no longer hardcodes anOEBPS/prefix. Case-insensitive fallback retained for EPUBs with mismatched image folder casing.
2026-03-25 (11)
- Fixed reader progress bar jumping to 100% immediately when navigating to the second chapter in short EPUBs (2 chapters): changed progress formula denominator from
total - 1tototalso 100% is only reached after fully scrolling through the last chapter. Same fix applied to PDF page progress.
2026-03-25 (10)
- Added bookmark feature:
bookmarkstable in DB (migrate_create_bookmarks):filename,chapter_index,scroll_frac,chapter_title,note,created_at- New API endpoints:
GET/POST /library/bookmarks/{filename},PATCH/DELETE /library/bookmarks/{id},GET /api/bookmarks - Bookmark button in reader header (orange, bookmark icon): opens modal with optional note field
- Bookmark modal closes on Escape, backdrop click, or Cancel
- Reader supports
?bm_ch=N&bm_scroll=FURL params to jump directly to bookmarked position (overrides saved progress) - Library sidebar: Bookmarks section in Library nav with live count badge
- Library
#bookmarksview: card list showing cover, book title, author, chapter, note, date, Go-to and Delete buttons
2026-03-22
- Added blueprint/technical documentation structure in
docs/. - Completed router split and bootstrap structure (
main.py, routers, migrations, DB pool). - Expanded media support to EPUB/PDF/CBR/CBZ in import and scan flows.
- Expanded Home UI with:
- import dropzone for EPUB/PDF/CBR/CBZ
- search functionality
- alignment matching Library (search top-right, dropzone below)
- Updated Library import texts and drag/drop filtering for multi-format support.
- Expanded Library
Newview with:Grid/Listtoggle- column visibility filter in
List - multi-select + bulk
Remove from New - selection only in
Listmode Shift+clickrange selection on checkboxes
- Added route:
POST /library/new/mark-reviewed(bulk setneeds_review=false). - Improved library performance:
/api/libraryfast-path (no full rescan on every page load)- optional
rescan=true/include_file_info=true - SQL optimizations in
list_library_json() - additional DB indexes for scale
- Restored
/api/homefull dataset output:continue_readingshorts_unreadnovels_unreadshorts_readnovels_read
- Explicitly filtered series books out of Home sections.
- Corrected Home read ordering:
shorts_readandnovels_readnow show oldest first (ORDER BY MAX(read_at) ASC). - Restored Statistics page by returning the full
/api/statspayload required for charts, favorites, top books, and reading history. - Improved backup implementation:
- Dropbox token stored encrypted in DB
- Dropbox backup root configurable via web UI and stored encrypted in DB
- versioned snapshots + object-store deduplication in Dropbox (
library_snapshots/library_objects) - configurable snapshot retention (
snapshots to keep) via backup settings - object pruning based on retained snapshots
- scheduled backup (enable + interval in hours)
- backup runs as a background process, so page navigation does not block execution
- stale running state recovery after restart/crash (old running logs marked interrupted/error)
- dry-run support in the new flow
- Updated Docker image with
postgresql-clientforpg_dump. - Multiple test builds pushed to
gitea.oskamp.info/ivooskamp/novela:dev.
2026-03-25 (9)
- Fixed backup progress not visible immediately after clicking Run Live Backup: sidebar
loadBackupProgress()is now called directly from the backup page after a successful start - Fixed backup status timestamp showing wrong relative time (e.g. "1h ago" for a recent backup): server UTC timestamps without timezone suffix are now correctly interpreted as UTC in the browser
- Fixed backup status bar using browser-default purple link color: now uses
var(--accent)orange consistent with the rest of the UI - Added password manager ignore attributes to App Key and App Secret fields:
data-1p-ignore(1Password),data-lpignore="true"(LastPass),data-form-type="other"(1Password v8)
2026-03-25 (8)
- Added Dropbox OAuth2 refresh token support (no redirect URI needed):
- New endpoints:
POST /api/backup/oauth/prepare(generates auth URL) andPOST /api/backup/oauth/exchange(exchanges code for refresh token) _dbx()now prefers refresh token mode (app key + secret + refresh token) with automatic renewal; falls back to legacy access token for backwards compatibility- App key and secret stored encrypted in
credentialstable (dropbox_app_key,dropbox_app_secret) DELETE /api/backup/credentialsnow also cleans updropbox_app_keyanddropbox_app_secret- Backup page updated with two-step OAuth flow UI (app key/secret → auth URL → paste code)
- Settings status shows
• refresh tokenor• legacy tokenindicator
- New endpoints:
2026-03-25 (7)
- Added backup progress tracking:
GET /api/backup/progressreturns{running, done, total, phase}; sidebar shows live file count (e.g.312 / 652 · uploading) polling every 3 s while running
2026-03-25 (6)
- Fixed
GET /api/backup/healthmissingschedule_enabledandschedule_interval_hoursin response (values were read but not returned; Health card showed "disabled" even when schedule was on) - Added backup status indicator in sidebar above Rescan library button: coloured dot + status text (OK/running/failed/never), links to
/backup, auto-refreshes every 8 s while running, shows time-ago for last successful backup
2026-03-25 (5)
- One-time path migration: all 571 existing library files moved to correct format-prefixed structure (
epub/,pdf/,comics/) and all DB references updated (migrate_paths.py --execute) - Empty old publisher root directories pruned after migration
- Recovery script (
recover_decock049.py) written; confirmed file was added after last Dropbox backup so not recoverable — to be re-imported manually
2026-03-25 (4)
- Added {publisher} directory to PDF and CBR/CBZ paths:
pdf/{publisher}/{author}/{title}.pdf,comics/{publisher}/{author}/{title}.cbr/cbz— consistent with EPUB structure
2026-03-25 (3)
- Fixed CBZ extension in import:
common.make_rel_pathalways generated.cbrfor CBZ files; now acceptsextparameter;library.pypasses actual suffix so CBZ files land atcomics/{author}/{title}.cbz - Added missing
GET /download/{filename}endpoint (referenced in book.html but was 404) - TECHNICAL.md fully rewritten: added File Storage Paths section, complete endpoint lists for all routers including settings.py, corrected path documentation
2026-03-25 (2)
- Fixed PDF metadata editing (PATCH /library/book):
_sync_epub_metadatais now only called for.epubfiles; PDFs update DB only_make_rel_pathnow includes the format prefix matching import: EPUB →epub/{publisher}/{author}/…, PDF →pdf/{author}/{title}.pdf, CBR/CBZ →comics/{author}/{title}{ext}; previously files were moved outside their format directory on metadata save
- Fixed PDF reader (infinite loading screen):
reader_pagenow passesformat(epub/pdf/cbr/cbz) toreader.html- Added
GET /api/pdf/info/{filename}endpoint returning{"page_count": N} reader.htmlbranches onFORMAT: PDFs render page images from/library/pdf/{filename}?page=N, EPUB flow unchanged- PDF progress tracked per page; keyboard and button navigation work identically to EPUB
Edit EPUBbutton in Book Detail hidden for non-EPUB files
2026-03-23
- Added
All booksGrid/List toggle in Library:- same columns as
Newview (Publisher, Author, Series, Volume, Title, Has cover, Updated, Genres, Sub-genres, Tags, Status) - column visibility filter in
Listmode - no selection checkboxes or bulk actions
- view mode and column visibility persisted separately in
localStorage(novela.all.viewMode,novela.all.visibleColumns)
- same columns as
- Added 1–5 star rating for books:
- stored in the database (
rating SMALLINT DEFAULT 0) - written to EPUB OPF as
<meta name="novela:rating" content="N"/>on rating change - written to CBZ
ComicInfo.xmlas<NovelaRating>N</NovelaRating>on rating change - CBR and PDF are DB-only (file format constraints)
- rating is recovered from file metadata during rescan/DB rebuild (
upsert_bookpreserves file rating over default 0) - stars displayed under the cover (outside
.book-info) in all grid views (Library, Home) - stars in grid cards are display-only (no click) to prevent accidental taps while scrolling
- stars in Book Detail are interactive (larger, 1.1rem), clicking same star removes rating
- star color: amber (
#c8a03a) for filled,rgba(200, 160, 58, 0.25)for unfilled — consistent across Library, Home, and Book Detail
- stored in the database (
- Added text colour setting to reader hamburger menu:
- 5 warm-tone presets from bright (
#e8e2d9) to dim (#938d86) - active preset shown with accent-coloured ring
- choice persisted in
localStorage(reader-text-colour) and restored on next open
- 5 warm-tone presets from bright (
- Increased spacing between hamburger button and back link in reader header (
margin-left: 1rem) to prevent accidental taps - Removed
Cover Missingauto-tag:- tag is no longer added on import, rescan, or grabber download
ensure_cover_missing_tag()removed fromcommon.py,library.py, andgrabber.py- startup migration removes all existing
Cover Missingtags from the database
- Fixed Tags/Genres/Sub-Genres not saving in book edit panel on desktop:
,(comma) now acts as a confirmation key alongside Enter inPillInputflush()added toPillInput: any text still in the input field is auto-confirmed when Save is clicked
- Fixed tag/genre search and tag-pill navigation being broken:
renderGenreViewwas filtering onb.genres(non-existent field); now usesbookGenres(),bookSubgenres(),bookPlainTags()renderSearchResultshad the same bug; search now covers title, author, genres, sub-genres, and tags
- Made reading progress monotonic across devices:
POST /library/progress/{filename}now rejects any save whose(chapter_index, scrollFrac)is not strictly ahead of the stored position (returns{"ok": true, "skipped": true})- prevents device A from overwriting further progress saved by device B when switching between devices without closing the book
- also prevents bookmark-based backward navigation (e.g. jumping back to correct an earlier chapter) from clobbering the furthest-read position
- progress reset remains via the explicit Read/Unread actions, which clear the
reading_progressrow