Conversation
ajslater
commented
May 9, 2026
Owner
- Fixes
- Fix occasional recreation of comics on docker or network filesystems.
- Fix double polling of some libraries.
- Fix Codex sometimes hanging on shutdown after Ctrl+C.
- Fix PDFs rendering OCR overlay text.
- Fix PDFs sometimes not rendering at correct resolution
…ages. move ci contaiiner to compose
…ender so the menu isn't lost in the center of the screen
…justing template settings in debug mode
commit c76660006840abb36aa37d7355d5c7e242babebf Merge: b2b5a011a be94a0a Author: AJ Slater <aj@slater.net> Date: Sun Apr 5 15:49:11 2026 -0700 Merge branch 'develop' into select-multiple commit b2b5a011ab207a7307505b092877f789e1a66ab3 Author: AJ Slater <aj@slater.net> Date: Sat Apr 4 22:15:54 2026 -0700 fix card menu hover highlighting commit 5a98fe23d89be5e11fa46ada927100ccd3e08cda Author: AJ Slater <aj@slater.net> Date: Sat Apr 4 22:13:37 2026 -0700 new design for select many mode. no settings drawer involvement. reconfigure cards commit 82c612e3f77eec90b02465174b49fc8552871686 Author: AJ Slater <aj@slater.net> Date: Sat Apr 4 01:50:11 2026 -0700 add github config to source include. remove dockerhub config commit b02bd450f4598300d78433a2d749741872d14894 Author: AJ Slater <aj@slater.net> Date: Sat Apr 4 01:42:32 2026 -0700 update deps commit d7bbc875e7936d808259c9ba726127fcc3af905e Author: AJ Slater <aj@slater.net> Date: Fri Apr 3 23:56:12 2026 -0700 select many toolbar commit 246d5ecda59aaabb83e41db3ad32ec552804b9ae Author: AJ Slater <aj@slater.net> Date: Fri Apr 3 22:23:24 2026 -0700 select many feature first attempt
The build-icons target invoked the macOS-only Inkscape binary for comic.svg because cairosvg couldn't resolve the external xlink:href="logo.svg" reference that comic.svg used to contain. That xlink was inlined as a nested <svg> in v1.11.0, so the inkscape branch is dead code — and cairosvg itself still needs libcairo as a system dep, which the codex-ci Docker image does not install. Switch to resvg-py: a Python wheel that bundles a statically-linked Rust binary. Zero system libs, works identically on macOS and the Debian CI image. Renders comic.svg byte-identical to the previous output. Also adds bin/sync_comic_logo.py + a sync-comic-logo make target to re-inline logo.svg into comic.svg (recoloring every fill/stroke to gray and dropping the black-circle background) for whenever logo.svg changes. Fixes a "scelera" typo in logo.svg/logo-maskable.svg picked up while testing the sync. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ery rarely by the dev
The poller's main loop computed timeouts in two passes: ``_get_min_timeout`` to find the soonest due library, then ``_poll_due_libraries`` to actually poll them. Both called ``_get_poll_timeout`` per library, and that helper emits the INFO line "Library X waiting for manual poll." as a side effect for every ``poll=False`` library it sees. Result: the line appeared twice in the log every cycle a library was due — most visibly for the ``custom-covers`` library, which is always poll=False. Fold the two passes into one method that polls each due library inline and folds its post-poll ``poll_every`` into the running min, so each library is inspected exactly once per cycle. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix snapshot diff matching files to directories on inode collision `Snapshot._inode` collapses cross-filesystem inodes into a single keyspace (`_ignore_device=True`), so a comic file's stored inode could collide with an unrelated directory's inode on a different mount. `_find_moved_paths` accepted any inode match as a rename, the importer ran with it, and `Comic.presave()` re-read disk stat at the directory target — leaving a "phantom Comic-as-folder" row in the database. The original comic file then re-imported as a fresh row on the next cycle, orphaning bookmarks. Add `_is_move_compatible` that rejects cross-type pairs (file ↔ dir) and, for file pairs, requires `st_size` to match. Real renames preserve both type and size; cross-device inode coincidences essentially never do. Directory `st_size` varies with entry count, so the size check applies to files only. Bug evidence in the production database I was given: 19 Comic rows had `stat[0]` directory-mode bits set and paths pointing at real folders on disk (e.g. `/comics/Marvel/Wolverine Saga (1989)`, dir mode, ino 1104 — same ino as the Powers Gods #1.cbz comic that used to live at that row pk before the diff "moved" it). A follow-up migration cleans those up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * add migration to delete phantom Comic-as-folder rows Companion to the `_is_move_compatible` guard. The guard prevents new corruption; this cleans up rows that the inode-collision bug already turned into phantom Comic-as-folder rows in existing databases. A row qualifies for deletion when: - Its `path` lacks a comic suffix (.cbz/.cbr/.cb7/.cbt/.pdf), and - Its stored `stat[0]` has the directory mode bit set, OR `Path(path).is_dir()` returns True on disk now. Both signals are required so a genuinely-named comic with a transient stat read isn't deleted by mistake. We also reuse migration 0039's sentinel-stat probe: if no comic-suffix path can be `stat()`'d as a real file, the comics volume is unmounted and the migration skips entirely rather than risk deleting against a phantom-empty disk. Bookmarks attached to the deleted rows are lost. The number of affected comics per install is small (production sample: 19 corrupt rows out of 877). Recovering bookmarks would require capturing them before delete and re-attaching after the next poll re-creates the correct Comic at the original path; the complexity isn't justified for the blast radius. Migration 0039's `_remove_non_comic_comics` only targeted rows with `page_count == 0` — the signature of the original importer bug it was written for. Rows corrupted by the inode-collision path inherit their original page_count and slipped through that filter, so a separate cleanup is needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The May-1 fix that stopped flagging every comic modified when inodes rotate (689bbc4) correctly silenced the false-modify storm but left DB stat rows permanently lying about disk reality afterward — once a Docker bind-mount remount rotated the kernel's inode space, the DB's stored inodes no longer pointed at anything on disk, and ``_find_moved_paths`` had a stale lookup table from there on. The ``_is_move_compatible`` guard added in the previous PR suppresses the corruption that produced, but legitimate cross-remount renames still degrade to delete+add (and the user loses bookmarks on the recreated comic.pk). Phase 3 closes the loop. ``SnapshotDiff`` now emits ``StaleStatRefresh`` payloads for every ``data.unchanged`` path whose mtime+size still match but whose inode has rotated. The poller writes those fresh stats back to the appropriate model with one ``bulk_update`` per affected table, with ``fields=["stat"]`` only — ``updated_at`` is intentionally NOT bumped, since the file's content is unchanged and bookmark "fresh" semantics must hold. The refresh is skipped on ``force=True`` polls because force routes every path through the import pipeline (where ``presave`` already rewrites stats), and we don't want to double-write. To target the right model with one bulk write per table, ``DatabaseSnapshot`` now tracks the source model for each path in a new ``_path_to_model`` map; ``Snapshot.model_for_path`` exposes it, and ``DiskSnapshot`` leaves the map empty. Operational characteristics: - Zero DB writes when inodes are stable (the common case). The diff loop runs in O(unchanged) time but the per-path inode comparison is just a tuple equality. - One ``bulk_update`` per affected model on a remount (Comic, Folder, FailedImport, CustomCover separately). - Idempotent: a second cycle after a successful refresh sees inodes matching disk and emits zero refreshes. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.11.0's parallel cover rendering inherits the foreground process group, so SIGINT killed workers mid-call inside ``call_queue.get()`` — leaving the pool's manager thread broken and ``p.join()`` hung in the librarian's interpreter-shutdown atexit hook, which in turn hung the main process at ``librarian.join()``. Workers now ignore SIGINT; ``stop()`` terminates in-flight workers and waits for the manager thread before returning. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix dev Vite HMR blocked by CSP / allowedHosts mismatch
The HMR CSP overlay hardcoded ``http://localhost:5173`` while
django-vite renders every script ``src`` as
``http://{socket.gethostname()}:5173/...``. On any box where the
hostname isn't ``localhost`` (i.e. every real macOS install) the
browser blocked the Vite client with a CSP violation regardless of
which URL the user navigated to. Vite's own ``allowedHosts`` was
``[hostname()]`` only, so navigating via ``http://localhost:9810``
was rejected by Vite even when the CSP would have permitted it.
- Compute ``VITE_DEV_SERVER_HOST`` once (lowercased — CSP source
expressions are case-sensitive, ``socket.gethostname()`` isn't on
macOS) and reuse it for both the CSP overlay and ``DJANGO_VITE``.
- Add ``localhost``, ``127.0.0.1``, and ``[::1]`` to Vite's
``allowedHosts`` so loopback access works alongside the system
hostname.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* remove hostname examples from comments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DHCP often hands out an FQDN as the system hostname (e.g. ``box.example.com``). The FQDN resolves via WAN DNS to the box's public IP, and most consumer routers don't NAT-loopback that back to the LAN — so the browser can't reach the dev server even though django-vite and Vite were happy with the matching host. Strip the domain off any non-``.local`` FQDN and substitute the mDNS form. Single-label hostnames and ``*.local`` are passed through untouched. Same logic in both places so the CSP origin and Vite's allowedHosts stay aligned with the URL the browser actually uses. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Some scanned PDFs draw their OCR layer with rendering mode 0 (visible) on top of the page's rasterized scan, doubling the text under any renderer that respects the content stream — including PDF.js as embedded by vue-pdf-embed. Setting ``textLayer={false}`` client-side doesn't help because that prop only gates the selectable overlay, not text drawn from the content stream. Forward the new ``hide_text`` kwarg from comicbox >= 3.0.1 (which forwards to comicbox-pdffile >= 0.5.1) when ``?hide_text=1`` is present on the page request. The PDF / pixmap response still contains the text content — only the rendering mode changes — so the selectable overlay continues to work. The pyright/ty ignores cover the dev gap until ``pyproject.toml`` deps are bumped to the released versions of comicbox / comicbox- pdffile. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Vite HMR CSP overlay landed in #738, but the PWA service worker captures its CSP from the response that installed it and keeps using that snapshot until its own JS bytes change. Existing SWs in the browser pre-date the overlay, so cross-origin fetches to ``http://hostname:5173`` still hit a stale ``connect-src`` and get blocked from inside the SW's ``fetch`` handler — even after the page CSP is correct. ``cache_page(COMMON_TIMEOUT)`` on the SW URL compounded this: SW endpoints should never be server-cached because the browser uses byte-level diff to detect updates. - Skip non-GET and cross-origin requests in the SW ``fetch`` handler so Vite HMR (and any future cross-origin asset) is governed by the page CSP, not the SW's snapshot. Also a more conventional SW shape: there's no point caching cross-origin responses we can't reason about. - Add ``self.clients.claim()`` in ``activate`` to pair with the existing ``skipWaiting()``, so the replacement SW takes over open tabs in one reload instead of two. - Drop ``cache_page`` from the SW URL. Browsers handle SW update detection themselves; a server-side cache only pins content and CSP. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.