Skip to content

v2.1.0 - #792

Merged
ajslater merged 390 commits into
mainfrom
develop
Jul 1, 2026
Merged

v2.1.0#792
ajslater merged 390 commits into
mainfrom
develop

Conversation

@ajslater

@ajslater ajslater commented Jul 1, 2026

Copy link
Copy Markdown
Owner
  • Features

    • Rename comic files to the comicbox naming scheme when editing tags or
      tagging online.
  • Fixes

    • Online tagging matches already-tagged comics more accurately by searching
      embedded metadata instead of the filename (comicbox 4.0.4).
    • Renaming a file no longer fails when a series or title contains a slash
      (comicbox 4.0.4).
    • Clearing a search returns to the top collection instead of stranding you
      at a lower level.

ajslater and others added 30 commits April 23, 2026 10:32
iOS Panels (and other Basic-Auth OPDS clients) intermittently hit
sqlite3.IntegrityError: FOREIGN KEY constraint failed when settings
or bookmarks were saved. Two interacting bugs caused it:

- Janitor cleanup_sessions used `if not session.get_decoded():` to
  detect "corrupt" sessions. get_decoded() returns {} for both real
  decode failures and legitimate anonymous sessions with no stored
  data — exactly what Basic-Auth OPDS clients produce. The nightly
  task was wiping valid session rows. Replaced with a direct
  signing.loads() call so only genuine signature/decode failures are
  flagged.

- _ensure_session_key returned the cookie's session_key without
  verifying the row still exists. With cached_db the session loads
  from cache without rechecking, so a stale cookie key would slip
  through and cause an FK violation when used as SettingsBrowser /
  SettingsReader.session_id. Now we verify existence and flush+save
  to cycle the key when the row is gone.

Either fix alone closes the user-visible error; both together also
stop the underlying churn that created the bad state.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
The same stale-session_key FK-violation pattern existed in two more
places that also write rows whose session FK can be stale:

- BookmarkAuthMixin.get_bookmark_auth_filter — feeds session_id into
  Bookmark.objects.bulk_create / bulk_update.
- ReaderSettingsBaseView._get_bookmark_auth_filter — feeds session_id
  into SettingsReader.objects.create.

Both used the old `if not session.session_key: save()` pattern that
trusts the cookie. Hoist the validated _ensure_session_key helper
from SettingsBaseView up to AuthMixin so every auth-aware view shares
one implementation, and switch both call sites to it. BookmarkAuthMixin
now extends AuthMixin to inherit the helper. BookmarkFilterMixin is
unchanged — it's read-only (filter Q only) and a missing session
correctly returns no rows.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…611)

The /admin/stats endpoint's user_registered_count and
auth_group_count fields have been silently returning 0 since at
least Sep 2024. The Stats tab in the admin UI shows 0 registered
users even on installs with multiple accounts.

Root cause: _add_config tried to rename the per-model count keys
produced by _get_model_counts:

    config["user_registered_count"] = config.pop("users_count", 0)
    config["auth_group_count"]      = config.pop("groups_count", 0)

But _get_model_counts builds keys via
``snakecase(model.__name__) + "_count"``. For Django's
``django.contrib.auth.models.User`` / ``Group`` that produces
``user_count`` and ``group_count`` (singular). The pop()s with the
plural names never matched, so the default ``0`` won every time —
and the actual ``user_count`` / ``group_count`` keys were left
orphaned in the dict, then dropped by the StatsConfigSerializer
which only declares ``user_registered_count`` /
``auth_group_count``.

Fix: pop the right source keys.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Reader page: fix load-progress spinner that never appeared

Two stacked bugs in the same setTimeout:

1. Non-arrow callback lost ``this``. The function ran with the
   timer's context, not the component's, so ``this.loaded`` and
   the write below were both no-ops.

2. The write targeted ``this.loading``, which has never been a
   data field on this component. The template binds the spinner
   to ``showProgress`` (line 15: ``v-if="showProgress && !loaded"``).

So even if the arrow had been there from the start, the spinner
still wouldn't have rendered — both bugs had to land at once.
Net: ``LoadingPage`` has been dead code for slow image loads.

Switch to an arrow function and write ``showProgress`` instead
of ``loading``. Stash the timer ID so ``beforeUnmount`` can
clear it; a fast page swap mid-delay would otherwise fire the
write on a torn-down component.

Implements B1 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Reader store: fix arc-mtime fallback that itself 500'd

``loadMtimes`` builds an arcs list of ``{ group, pks }`` from
``this.arcs``; if the dict is empty the function previously
fell back to ``arcs.push({ r: "0" })``. The comment noted that
"No arcs is a 500 from the mtime api" — the fallback was added
to dodge that 500 — but the wrong-shape fallback also produced
a 500 because the API expects ``group``/``pks`` keys, not ``r``.

Use the canonical shape so the fallback actually works.

Implements B2 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* iOS PWA download: pass the object URL to revokeObjectURL, not the Blob

``URL.createObjectURL(blob)`` returns a ``blob:...`` URL string;
``URL.revokeObjectURL`` must receive that same string to free the
mapping. The previous code passed ``response.data`` (the Blob
itself), which silently no-op'd and leaked one object URL per
download.

On iOS PWAs this matters more than elsewhere because the leak
accumulates across the user's session and can't be reclaimed
short of reloading the app.

Implements B6 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Browser API: stop mutating caller's settings in getGroupDownloadURL

``getGroupDownloadURL`` did ``delete settings.show`` on the
caller's object before building the URL. Side-effect: any caller
that re-used the settings dict after the download-URL build saw
its ``show`` key silently vanish. This was probably fine when
the function was first written but it's a footgun now that
settings flow through a Pinia store.

Destructure-and-spread to drop ``show`` without touching the
input.

Implements B8 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Auth store: make logout awaitable + clear state unconditionally

Two changes to the logout action:

1. ``async`` so callers can ``await``. The current call site
   (``auth-menu.vue``) fires and forgets, but a future UX pass
   that wants to disable the button while logout is in flight
   needs the promise.

2. Clear ``this.user`` in ``finally`` rather than only on
   success. The user clicked "log out" — UI should reflect the
   logged-out state immediately, regardless of whether the
   server-side logout endpoint succeeded. Server-side cookies
   that survive the network failure will get cleaned up by the
   next 401.

Implements B7 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Browser filter menu: stop double-rendering each filter row

``<v-list>`` was passed both ``:items="vuetifyItems"`` AND a
default-slot ``v-for`` over the same list. Vuetify renders the
items prop into ``v-list-item`` children directly, so every row
was being built twice — once by the prop, once by the manual
``v-for``. Visible to users on filter menus with large choice
lists (genres, characters, etc.); each row appeared duplicated
and the DOM cost doubled.

Drop the prop. Keep the ``v-for`` because it carries the custom
``#append`` slot for ``metronName`` rendering. ``:model-value`` /
``@update:selected`` still drive selection state via each list-
item's ``:value`` prop.

Implements B10 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Admin job-tab: remove API-fetching click handler from expanded panel

The expanded status panel had ``@click="loadAllStatuses"`` on its
container div. Any click inside the panel — including clicks on
child elements that bubbled — refetched the entire status map.
Probably copy-pasted as a "refresh on click" gesture, but it
fired far too often: a user inspecting a long status list would
trigger N API calls just from glancing around.

The status data is pushed through the websocket already
(socket.js fans librarian notifications into the admin store),
so the panel is up-to-date without a manual refresh.

Implements B11 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Reader store: handle bookmark-write errors instead of silently rejecting

``setRoutesAndBookmarkPage`` awaited ``_setBookmarkPage`` but
didn't catch its errors. On a network blip the promise rejected,
the bookmark didn't persist, and the failure became an unhandled
rejection in the browser console — not visible to the user, not
retried, just lost.

Wrap in try/catch. The local page state stays where it is (the
user is reading forward; the bookmark catches up on the next
write), but the failure is logged so debugging surfaces. A
proper user-visible toast + retry path is broader UX work
tracked in the plan.

Implements B3 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Metadata dialog: clear progress timer on unmount

``updateProgress`` chains itself via ``setTimeout`` until the
metadata loads or progress reaches 100. The timer ID was never
stashed, so closing the dialog mid-animation left the chain
running — each tick fired on a torn-down component, writing
``this.progress`` and re-scheduling against now-null refs.

Stash the timer ID and clear it in ``beforeUnmount`` so the
chain stops cleanly when the dialog goes away.

Implements B12 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Reader pager: key dynamic component on identity to force remount

``<component :is="...">`` without ``:key`` lets Vue reuse the
existing instance across an ``is`` change when the components
share enough surface (props, name). For the reader's
vertical/horizontal pager swap that's wrong: scroll listeners
attached by the previous mode persist, the new mode's
``mounted`` runs against stale internal state, and any
abort/teardown logic in ``beforeUnmount`` never fires.

Add ``:key="component.name"`` so the swap is a true unmount +
remount — old listeners go away, new mode starts clean.

Implements B13 of tasks/frontend-perf/01-correctness-bugs.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Metadata dialog: lint cleanup for the B12 fixup

Vue option-order rule: ``beforeUnmount`` belongs above
``methods``. Block-comment style required for the multi-line
explanation in ``updateProgress``. Both surfaced when running
eslint on the prior commit; pure cleanup, no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
ajslater and others added 29 commits June 21, 2026 22:09
run-test-proxy.sh launches a local nginx that wraps nginx/default.conf
(a linuxserver-style server block) so you can test Codex behind a reverse
proxy under a url_path_prefix subpath.

Previously it regenerated the cert and all configs into a temp dir on
every run. Generate that scaffold once into nginx/test-proxy/ and check it
in: the self-signed localhost cert, the events/http wrapper, the ssl.conf
stand-in, and default.conf adapted for standalone use (80/443 -> 8080/8443,
absolute ssl include rewritten). A plain run now just launches nginx; only
the gitignored tmp/ working dir is created at run time. --regenerate
rebuilds the scaffold from default.conf after edits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eliminate the nginx/ directory:
  - nginx/run-test-proxy.sh -> bin/run-test-proxy.sh
  - nginx/test-proxy/       -> test-proxy/
  - drop nginx/default.conf; test-proxy/server.conf is now the committed,
    hand-edited source of truth (no more regenerate-from-default step)

Drop the Docker-based nginx dev proxy in favor of the native one:
  - remove the unused nginx service from compose.yaml
  - delete bin/dev-reverse-proxy.sh (was broken: referenced a missing
    nginx/nginx.yaml) and point `make dev-reverse-proxy` at run-test-proxy.sh

The script now just launches nginx against the committed test-proxy/ scaffold
(regenerating only the self-signed cert, and only with --new-cert or if
absent); edit test-proxy/server.conf to change ports/backend/routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The localhost cert is throwaway and regenerated on first run by
bin/run-test-proxy.sh, so there's no need to publish it. Gitignore
cert.pem / cert.key and remove them from the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The location block defined its own proxy_set_header (Remote-User), which
made nginx drop every server-level proxy_set_header — including the
WebSocket Upgrade/Connection headers — so sockets never upgraded.

Move Remote-User up to the server block so the location inherits all the
headers, and fix two issues that inheritance then exposed:

  - Connection was hardcoded "Upgrade", which 400'd normal requests once
    actually applied; drive it from a $http_upgrade map ($connection_upgrade)
    so only real WS requests upgrade.
  - X-Forwarded-Host was $server_name ("_"); with codex's
    USE_X_FORWARDED_HOST=True that made Django 400 every dynamic request
    (DisallowedHost). Forward $host instead.

Verified through the proxy: normal HTTP/HTTPS 200, static 200, ws/wss
handshakes return 101 Switching Protocols.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chrome flagged the tagging tab's single v-form for cramming multiple
credential groupings into one form. Split the Metron and Comic Vine
credential panels into their own sibling forms (the password-free
defaults sections keep the outer form), so each form maps to one save
action.

Add autocomplete to the credential/URL fields to keep these
third-party service secrets out of the browser password manager:
new-password on the password/API-key inputs (which Chrome honors where
it ignores off), off on the username and URL fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Add read-only library flag to block tag writes

Mark a Library read_only to protect its comics from all archive-mutating
operations (Edit Tags + Tag Online). Enforced server-side at the single
resolve_comic_pks funnel and again at the task executors; mixed selections
write only editable comics and report a skipped count. The metadata payload
gains an `editable` aggregate that hides the edit/tag buttons when nothing
in the selection is writable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix undefined updateLabel warning in admin library table

The edit dialog bound :label="updateLabel", but updateLabel was never
defined on the component, so Vue warned on every render once a library
row existed. The dialog's label prop already falls back to the table
name, so drop the broken binding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Declare collections as set[str] so the StrEnum Collection members
unify with the str tuple from valid_nav_collections; set is invariant
so set[Collection] |= set[str] was rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AdminUserChangePasswordView only implemented `put`, but the admin UI
posts to /admin/users/<pk>/password, so the request hit 405 Method Not
Allowed and surfaced as a generic "unknown error". Rename the handler
to `post` to match the frontend and every sibling admin action endpoint.

Add a regression test asserting the POST returns 202 and actually sets
the password. Bump to v2.0.5.

Reported by @petternstrm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Optionally rename comic files to the comicbox (comicfn2dict) scheme after
writing tags. Offered when editing tags, during online tagging, and as an
admin tagging default.

- TagWriter renames written archives via Comicbox.rename_file() and syncs
  the DB watcher-aware: unwatched libraries get a targeted files_moved
  ImportTask (path + metadata re-read); watched libraries are left to the
  filesystem watcher's move detection. Collision-safe (skip + report).
- BulkTagWriteTask gains a `rename` flag; manual edit and online tagging
  (scan auto-write, stored-id prefetch, by-id, and deferred prompts) all
  thread it through. Rename-only (no tag changes) is supported.
- Admin default ComicboxTaggingDefaults.rename_files (migration 0046) plus
  serializer field and Tagging-tab toggle; both flows fall back to it.
- Edit panel: rename toggle, live single-comic filename preview, full
  per-comic old -> new preview list in the confirm dialog, labels that
  pluralize for multi-comic and container edits, and an always-on
  confirmation for renames (higher-risk than a tag write).
- Preflight endpoint returns a capped per-comic filename preview list.

Tests: tests/test_tag_writer_rename.py, tests/test_tagging_rename_wiring.py,
frontend/tests/unit/rename-toggle.test.js (plus launcher-dialog updates).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the comicbox-scheme name equals a file's current name it's a no-op,
not a rename, so the edit panel now reports it as such:

- The confirm dialog lists only files that will actually change, notes how
  many already match, and disables confirm (with an "all already match --
  nothing to rename" message) when nothing would change.
- The inline single-comic preview isn't styled as a pending change for a
  no-op and tooltips "Already matches the comicbox scheme".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entering a search redirects the browser down to lowestShownCollection;
clearing it never redirected back out, leaving the user at a deep
collection root (e.g. series) with no parent breadcrumbs / up-arrows.
Reverse the redirect in _validateSearch when a search is cleared from
that redirected-into root. Adds a browser-store regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a single comic is opened and renaming is the only pending action, the
Save button is disabled once the preview shows the comicbox-scheme name
matches the current filename -- there's nothing to do. Tag edits still save,
multi-comic selections stay enabled (each file's outcome is only known once
the confirm dialog previews them all), and the button stays enabled while the
preview is still loading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ajslater
ajslater merged commit f1e3575 into main Jul 1, 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