Skip to content

v1.11.5 - #744

Merged
ajslater merged 238 commits into
mainfrom
develop
May 9, 2026
Merged

ajslater merged 238 commits into
mainfrom
develop

Conversation

@ajslater

@ajslater ajslater commented May 9, 2026

Copy link
Copy Markdown
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

ajslater added 30 commits April 4, 2026 22:21
…ender so the menu isn't lost in the center of the screen
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
ajslater and others added 29 commits May 5, 2026 02:36
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>
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>
@ajslater
ajslater merged commit a57c51d into main May 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant