novela/docs/changelog-develop.md
2026-04-15 21:39:20 +02:00

50 KiB
Raw Blame History

Develop Changelog

2026-04-15 (1)

  • Reader: font size control in reading settings
    • New "Font size" slider (80150%, default 105%) in the settings drawer, between "Content width" and "Text colour"
    • Applies via CSS custom property --reader-font-size on #chapter-content
    • Persisted per-device in localStorage as reader-font-size — iPad and desktop each remember their own preference

2026-04-13 (1)

  • Edit metadata: comma-separated tag input fix
    • PillInput._add in book.js now 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, Adventure adds 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 by series_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

2026-04-12 (1)

  • Comics: series_volume support for annual series (issue numbers restart each year)
    • New series_volume VARCHAR(20) column on library (migration migrate_series_volume); default ''
    • Stored in EPUB OPF as <meta name="novela:series_volume" content="…"/>; read back by scan_epub
    • upsert_book inserts/preserves series_volume with the same COALESCE strategy as series_suffix
    • list_library_json ORDER BY now: publisher → author → series → series_volume → series_index → title
    • PATCH /library/book/{filename} reads series_volume from request body; persists to DB and OPF for both file-based and DB-stored books
    • Book detail page: displays (year) after series name when series_volume is 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 generates Series (Year/Vol) #Number from the current series fields
  • Bulk importer: series_volume support
    • 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-duplicates extended: now also checks (series, series_index, author) as fallback — detects duplicates even when the title format has changed; items must include series field
  • Library front-end: sorting within a series now respects series_volume before series_index
    • groupBySeries, renderAuthorDetail, renderPublisherDetail all sort by series_volume → series_index → series_suffix
    • getSeriesSlots refactored: when any book in the series has series_volume set, gap-detection runs per volume (year) independently — prevents #5 (1982) and #5 (1983) from colliding in the same slot
    • Slot index label shows (year) #index for 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 — sets archived for 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, allBooks is updated in place and sidebar counters are recalculated without a full page reload

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 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 <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=TOKEN links resolved to absolute URLs
    • fetch_chapter(): content from <div id="chapter">; 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 (<center><img src="../Images/break.png">) 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 (<center>) is replaced with <hr> instead of decomposing the <img>; 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 <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>
    • 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

2026-04-08 (7)

  • New scraper: CodeysWorldScraper (scrapers/codeysworld.py) for codeysworld.org
    • Matches all codeysworld.org URLs; 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/.html files in the same directory (audio/image links skipped)
    • fetch_chapter(): removes all <h1>/<h2> 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 <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)

2026-04-08 (5)

  • NiftyNewScraper: chapter content fix
    • new.nifty.org uses 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=1 instead 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 from children

2026-04-08 (4)

  • NiftyNewScraper added (scrapers/nifty_new.py) for new.nifty.org
    • Matches all new.nifty.org URLs; no login required
    • _to_index_url(): strips trailing /N (chapter number) so both index and chapter URLs can be used as entry point
    • fetch_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 detection
    • NiftyScraper.matches() updated: excludes new.nifty.org ("nifty.org" in url and "new.nifty.org" not in url)
    • NiftyNewScraper registered before NiftyScraper in scrapers/__init__.py

2026-04-08 (3)

  • Settings: develop mode added
    • New app_settings table (single row, id = 1) with develop_mode boolean; created via migrate_create_app_settings()
    • shared_templates.py: shared Jinja2Templates instance for all routers; develop_mode() registered as Jinja2 global so all templates can access it without explicit context injection
    • All 11 routers now import templates from shared_templates instead of each creating their own instance
    • New endpoints in routers/settings.py: GET /api/app-settings and PATCH /api/app-settings
    • Diagonal DEVELOP banner in the top-left of every page (CSS in static/sidebar.css, HTML in templates/_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

2026-04-08 (2)

  • Nifty scraper: fix _strip_email_headers now 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 causing Subject: 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

2026-04-08 (1)

  • Nifty scraper added (scrapers/nifty.py)
    • Matches all nifty.org URLs; 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_date from 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_count per 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.parsedateYYYY-MM-DD
  • xhtml.py: element_to_xhtml now handles bs4.Comment objects 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() calls POST /library/rating/… if the value has changed
    • rateBook() function removed from book.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 in library_cover_cache and has_cover = TRUE set in the library table — add_cover_to_epub is not called

2026-04-06 (1)

  • Search: filter for unread novels / unread shorts
    • New filter parameter on GET /api/search?q=…&filter=all|unread_novels|unread_shorts (default: all)
    • unread_novels: restricts to books with no reading sessions/progress and no Shorts tag
    • unread_shorts: restricts to books with no reading sessions/progress and with a Shorts tag
    • 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

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 via files_upload (unchanged); larger files via upload_session_startupload_session_append_v2upload_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 30 removed — all matching chapters are returned
    • New mode parameter on GET /api/search?q=…&mode=phrase|words: phrase (default) requires words in order (phraseto_tsquery); words requires 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
    • renderAuthorsView and renderPublishersView switched from activeBooks() to allBooks — consistent with renderAuthorDetail and renderPublisherDetail

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.sub pass 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 to make_chapter_xhtml
  • Search: switched from plainto_tsquery to phraseto_tsquery in the FTS WHERE clause
    • plainto_tsquery ANDs 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 apart
    • phraseto_tsquery requires all words to appear in sequence; ts_rank and ts_headline still use plainto_tsquery for 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
  • Library: archived books now visible in author and publisher detail views, with an indicator badge on the cover
    • renderAuthorDetail and renderPublisherDetail reverted to use allBooks (including archived) so archived books remain accessible from the author/publisher pages
    • New .badge-archived overlay (bottom-left of cover): dark circle with archive icon, consistent with existing badge-status and read-pill overlays; 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} handles db/… filenames; is_db flag passed to template
    • GET /api/edit/chapter/{index}/{filename} and POST …: DB branches query/update book_chapters directly; save calls upsert_chapter (updates content_tsv too)
    • POST /api/edit/chapter/add/{filename} and DELETE …: DB branches insert/delete with chapter_index shift via UPDATE … SET chapter_index = chapter_index ± 1
    • Title editing: header chapter-name replaced with a text input for DB books; pendingTitles map preserves unsaved titles across chapter switches (parallel to pendingContent); title-only dirty chapters correctly saved in Save All
    • insertBreak: scene-break image path is /static/break.png for DB books (vs ../Images/break.png for 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 for storage_type = 'db' books
  • Search: chapter titles now included in FTS
    • upsert_chapter prepends title to the plain-text input for to_tsvector: title + " " + stripped_html
    • GET /api/search: added OR LOWER(bc.title) LIKE LOWER('%…%') fallback for chapters whose title matches but content doesn't
    • Startup migration migrate_rebuild_chapter_tsv_with_title() rebuilds existing content_tsv values to include titles
  • Grabber: added DB/EPUB storage toggle on the Convert page
    • UI toggle above Convert button ("Save as: DB | EPUB file"); storageMode JS variable sent in POST body
    • POST /convert: reads storage_mode from body; stored in job as 'db' or 'epub'
    • _run_scrape: EPUB path builds chapters via make_chapter_xhtml, calls make_epub, writes file, calls upsert_book(storage_type='file'); DB path unchanged
    • done SSE event includes storage_type; conversion.js updates the download button label/action accordingly
  • EPUB → DB conversion: fixed double chapter title
    • _epub_body_inner strips 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 NavigableString crash: getattr(child, "name", None) is None used instead of hasattr(child, "name")NavigableString has name = None but no decompose() 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: added a:visited { color: var(--text-dim) } and a.active:visited { color: var(--accent) } to prevent the browser's default purple visited color

2026-04-03 (2)

  • DB-stored books (Fase 46): 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 new db/… 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_epub rewrites /library/db-images/… URLs back to OEBPS/Images/… paths, deduplicating by sha256; Content-Disposition: attachment response
    • Fase 6 — Full-text search: new routers/search.py with GET /search (page) and GET /api/search?q=… (FTS over book_chapters.content_tsv via plainto_tsquery('simple', q), ts_headline for snippets, ts_rank for ordering, LIMIT 30, excludes archived); new templates/search.html with 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 .epub files; delete modal text differs for DB vs file books
    • PATCH /library/book/{filename}: DB book branch added — skips file move, recomputes synthetic db/… filename via make_rel_path, applies same FK-safe rename pattern, updates book_chapters and bookmarks in addition to standard child tables

2026-04-03 (1)

  • DB-stored books (Fase 13): grabber now stores scraped books in PostgreSQL instead of EPUB files on disk
    • New book_chapters table: filename FK, chapter_index, title, content TEXT, content_tsv TSVECTOR; GIN index on content_tsv for future FTS
    • New book_images table: sha256 PK, ext, media_type, size_bytes; content-addressed imagestore at library/images/{sha2}/{sha256}{ext}
    • New storage_type VARCHAR(10) DEFAULT 'file' column on library; 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_path now handles media_type="db" → synthetic db/{pub}/{auth}/... filename
    • upsert_book and list_library_json updated to include storage_type
    • Grabber: _run_scrape stores chapters in book_chapters, chapter images in imagestore (absolute /library/db-images/ URLs embedded in HTML), cover in library_cover_cache; no EPUB file written
    • New GET /library/db-images/{path:path} endpoint serves imagestore files
    • Reader: GET /library/chapters/ and GET /library/chapter/ have DB branches for storage_type='db' books (query book_chapters directly)
    • 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 from library_cover_cache

2026-04-02 (1)

  • Added Restore functionality to the Backup page
    • New GET /api/backup/snapshots endpoint: lists available Dropbox snapshots (name + date parsed from filename, no downloads needed)
    • New GET /api/backup/snapshots/{snapshot_name}/files endpoint: loads a snapshot from Dropbox and returns all files with path, size, sha256, and whether the file currently exists locally
    • New POST /api/backup/restore endpoint: downloads file objects from Dropbox, writes to disk, and re-indexes via scan_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

2026-03-29 (10)

  • Duplicates: fixed updateCounts crashing with a TypeError (g.books.lengthg.length); the crash prevented renderGrid from 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
    • _duplicateGroups in library.js (Duplicates view): key now includes series_index when > 0, so different volumes of the same series are no longer grouped as duplicates
    • preload in grabber.py (Grabber): when the scraper returns a series_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 into static/theme.css; removed duplicate inline :root from all 15 templates and from library.css, book.csseditor.css keeps editor-specific vars (--danger, --header-h, --panel-w), reader.html keeps page-specific vars (--header-h, --footer-h, --content-w), backup.html keeps (--ok, --warn, --err)
    • Cover helpers: moved strHash, COVER_PALETTES, makePlaceholderCover, wrapText, truncate from library.js and book.js into books.js; removed from home.html and following.html
    • HTML escape: esc() added to books.js; removed from library.js, editor.js, and all 8 templates that defined it inline
    • SSE/EventSource: extracted shared connectConversionStream(job_id) and addLog() into new static/conversion.js; both index.html and grabber.html now call the shared function (removed ~70 duplicate lines)

2026-03-29 (7)

  • Search: extracted shared book helpers and search logic into static/books.js
    • _filenameBase, bookTitle, bookAuthor, tagValuesByType, bookGenres, bookSubgenres, bookPlainTags, filterBooks, setupSearchInput moved from library.js and home.html to 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 #0f0e0c background)
    • 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.png uses dark #0f0e0c background — renders as a native-looking iOS home screen icon

2026-03-29 (3)

  • Dockerfile: replaced unrar-free with proprietary unrar (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 — .cbr files that are actually ZIP or 7-zip archives now open correctly; added py7zr dependency 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; volume is included in the API call and matched against series_index in the DB

2026-03-28 (11)

  • Bulk Import: duplicate detection against existing library
    • New POST /api/bulk-check-duplicates endpoint: 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

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-delete endpoint 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/disk returns 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-reviewed JS: UI (allBooks update + renderGrid) now only runs after confirmed server success; catch no 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

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 #0f0e0c ring — always readable regardless of cover colour

2026-03-28 (4)

  • Added Temporary Hold status; renamed HiatusLong-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 in library.js replaces three identical inline badge blocks
    • Grabber: Temporary-Hold (gayauthors.org) now maps to Temporary Hold; Long-Term Hold passes through unchanged
    • Status dropdowns updated in Book Detail and Bulk Import

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)
    • IntersectionObserver defers both cover image loading and placeholder canvas drawing until cards enter the viewport — eliminates hundreds of upfront canvas ops that blocked the initial render
    • ETag caching on /library/list: server returns 304 Not Modified when nothing changed, client skips JSON parse and re-download
    • Single DOM pass in renderBooksGrid, renderDuplicatesView, renderSeriesDetail: canvas and img set up via card.querySelector immediately after innerHTML, removing a second iteration with document.getElementById per card
    • book_tags joined via json_agg in the main list_library_json() query, eliminating a separate SELECT * FROM book_tags query and Python merge loop
    • loadLibrary now 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 authors table: name (unique), url, created_at, updated_at
    • New routers/following.py: GET /following page, 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
  • Added Incomplete view to Library (#incomplete): shows all non-archived books where publication_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/makePlaceholderCover pattern as renderBooksGrid

2026-03-26 (2)

  • Fixed Book Builder page showing white background: library.css added to builder.html to load :root CSS variables and dark body background; all CSS variable references in builder.css aligned 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_drafts table (id UUID, title, author, publisher, source_url, chapters JSONB)
    • build_epub() in epub.py: builds a standards-compliant EPUB 2.0 ZIP from title/author/publisher/chapters; embeds inline CSS and break.png if present
    • normalize_wysiwyg_html() in xhtml.py: converts contenteditable HTML to EPUB-safe XHTML; handles scene-breaks, <blockquote class="author-note">, inline formatting, and <hr> → break image
    • routers/builder.py: draft CRUD, chapter CRUD (GET/POST/PUT/DELETE), normalize preview endpoint, publish endpoint (normalizes all chapters → builds EPUB → writes to library/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-note blockquote style added to static/epub-style.css

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 #rated URL 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 #0 for 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

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_count was missing from the cbr import in reader.py; /api/cbr/info/ was returning an error, causing page_count to 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 in reader.html that 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 to library table via migrate_series_suffix
    • series_index lower bound changed from 1 to 0 throughout (index 0 = special/prequel edition)
    • Volume field in the book editor changed from type="number" to type="text" — accepts "0", "1", "21a", etc.
    • Server parses the combined volume string into series_index (INTEGER) + series_suffix (VARCHAR) via parse_volume_str
    • File naming includes suffix: 021a - Title.epub
    • novela:series_suffix meta 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

2026-03-25 (14)

  • Added multi-select and bulk delete to All books List 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+click range selection on checkboxes
    • Delete selected button (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_epub fully rewritten: locates the OPF via META-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 of OEBPS/): image paths are now passed as full ZIP paths instead of stripping the root segment, and the image endpoint no longer hardcodes an OEBPS/ 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 - 1 to total so 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:
    • bookmarks table 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=F URL params to jump directly to bookmarked position (overrides saved progress)
    • Library sidebar: Bookmarks section in Library nav with live count badge
    • Library #bookmarks view: 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 New view with:
    • Grid/List toggle
    • column visibility filter in List
    • multi-select + bulk Remove from New
    • selection only in List mode
    • Shift+click range selection on checkboxes
  • Added route: POST /library/new/mark-reviewed (bulk set needs_review=false).
  • Improved library performance:
    • /api/library fast-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/home full dataset output:
    • continue_reading
    • shorts_unread
    • novels_unread
    • shorts_read
    • novels_read
  • Explicitly filtered series books out of Home sections.
  • Corrected Home read ordering: shorts_read and novels_read now show oldest first (ORDER BY MAX(read_at) ASC).
  • Restored Statistics page by returning the full /api/stats payload 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-client for pg_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) and POST /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 credentials table (dropbox_app_key, dropbox_app_secret)
    • DELETE /api/backup/credentials now also cleans up dropbox_app_key and dropbox_app_secret
    • Backup page updated with two-step OAuth flow UI (app key/secret → auth URL → paste code)
    • Settings status shows • refresh token or • legacy token indicator

2026-03-25 (7)

  • Added backup progress tracking: GET /api/backup/progress returns {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/health missing schedule_enabled and schedule_interval_hours in 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_path always generated .cbr for CBZ files; now accepts ext parameter; library.py passes actual suffix so CBZ files land at comics/{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_metadata is now only called for .epub files; PDFs update DB only
    • _make_rel_path now 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_page now passes format (epub/pdf/cbr/cbz) to reader.html
    • Added GET /api/pdf/info/{filename} endpoint returning {"page_count": N}
    • reader.html branches on FORMAT: 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 EPUB button in Book Detail hidden for non-EPUB files

2026-03-23

  • Added All books Grid/List toggle in Library:
    • same columns as New view (Publisher, Author, Series, Volume, Title, Has cover, Updated, Genres, Sub-genres, Tags, Status)
    • column visibility filter in List mode
    • no selection checkboxes or bulk actions
    • view mode and column visibility persisted separately in localStorage (novela.all.viewMode, novela.all.visibleColumns)
  • Added 15 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.xml as <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_book preserves 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
  • 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
  • Increased spacing between hamburger button and back link in reader header (margin-left: 1rem) to prevent accidental taps
  • Removed Cover Missing auto-tag:
    • tag is no longer added on import, rescan, or grabber download
    • ensure_cover_missing_tag() removed from common.py, library.py, and grabber.py
    • startup migration removes all existing Cover Missing tags 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 in PillInput
    • flush() added to PillInput: any text still in the input field is auto-confirmed when Save is clicked
  • Fixed tag/genre search and tag-pill navigation being broken:
    • renderGenreView was filtering on b.genres (non-existent field); now uses bookGenres(), bookSubgenres(), bookPlainTags()
    • renderSearchResults had the same bug; search now covers title, author, genres, sub-genres, and tags