Migrate deprecated EMAIL_* settings to Django's MAILERS - #816
Merged
Conversation
ajslater
marked this pull request as ready for review
August 22, 2026 02:37
Django 6.1 deprecates the discrete EMAIL_* connection settings (RemovedInDjango70Warning at django.setup()), and defining MAILERS makes reading the old names an AttributeError. - Replace the eight deprecated settings with an EMAIL_CONNECTION_OPTIONS dict (TOML/env layer, keyed by EmailBackend constructor kwarg) plus a MAILERS declaration pointing at the DB-aware DBEmailBackend. - get_email_connection_kwargs / get_email_from_address coalesce the EmailSettings DB row over EMAIL_CONNECTION_OPTIONS instead of the removed settings. - DBEmailBackend defaults its mailer alias so direct construction never hits the SMTP parent's pre-MAILERS settings fallback. - The admin test-send view builds the backend directly and calls send_messages(), dropping deprecated get_connection() and EmailMessage(connection=...). - Tests override MAILERS + EMAIL_CONNECTION_OPTIONS instead of EMAIL_BACKEND/EMAIL_HOST. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqa2ULBSaSVDwDSMEni1hf
ajslater
force-pushed
the
claude/django-email-host-password-jd8dr9
branch
from
August 22, 2026 02:42
308dbb0 to
decbee4
Compare
ajslater
added a commit
that referenced
this pull request
Aug 23, 2026
* fix(frontend): send passwordConfirm from profile change-password (#793)
The Profile dialog's self-service password change posted only
oldPassword + password to /api/v4/auth/password/change, but that
endpoint is rest_registration's ChangePasswordView whose serializer
requires password_confirm (camelCased passwordConfirm) — so the
request 400'd with "passwordConfirm field is required".
The dialog already collects and validates passwordConfirm; forward it
in the changePassword payload, matching change-password-dialog.vue and
the register/reset flows. Add a regression test asserting the field is
sent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps and version to v2.1.1. bump news
* test(frontend): work around vitest/valid-expect false positive on expect.any
The @vitest/eslint-plugin valid-expect rule misclassifies expect.any() as
chai's `.any` flag chain and reports "unknown modifier". Disable the rule on
the one nested assertion with a documented reason rather than weakening it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(onlinetag): drop dead effort option; model Metron's fewer requests
comicbox 4.0.5 no longer applies the effort knob to Metron tagging, and
Metron's search is now a flat two-step (series_list + issues_list) that
match mode does not change.
- Remove the vestigial `effort` option (serializer, task, resume params,
and test). It was collected by the API but never passed to comicbox's
OnlineSession.
- Count estimate calls-per-comic per source: Metron a flat 2, Comic Vine
keeps its per-mode 2/3/5. First-match-wins bills the costliest single
source; merge sums per-source calls. Mirrored in the launcher dialog.
- Resume view drops unknown persisted params so a pre-upgrade `effort`
key in the file-based cache can't crash the task rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* trim news
* refactor(onlinetag): derive source list + issue-id parser from comicbox
Two facilities codex hand-synced from comicbox now consume it directly:
- Source names: KNOWN_SOURCES and the task/serializer/frontend default
lists derive from comicbox's canonical SOURCE_NAMES tuple instead of
repeating {"metron","comicvine"} literals in four places. The frontend
gets it through the tagging choices JSON (build-choices), so a new
comicbox source propagates without hand-editing every site.
- Issue-id parsing: the two byte-identical trailing-int regex copies
(stored_id_prepass, explicit_id) collapse into one
issue_id.parse_issue_id built on comicbox's canonical PARSE_COMICVINE_RE.
It honors the real Comic Vine 4-digit long-key rule instead of grabbing
any trailing int; an unrecognized key returns None, which safely falls
back to search / rejects the id rather than guessing wrong.
No user-visible behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(onlinetag): scope the match-mode request-count hint to Comic Vine
The "~N requests/comic" tail in the match-mode hints only describes Comic
Vine, whose calls scale with match mode; Metron is a flat two-step search
regardless of mode. Drop the tail from the base hints and append a
"~N Comic Vine requests/comic" suffix only when Comic Vine is an active
source, so a Metron-only run no longer shows a count that doesn't apply.
The number derives from the existing COMICVINE_CALLS_BY_MODE constant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(fs): don't let one unreadable folder crash the library scan
The poller's DiskSnapshot._walk() called os.scandir() with no guard
around the directory open, so a single permission-denied folder (e.g.
a Synology /comics/#recycle bin) raised PermissionError that propagated
up and killed the LibraryPollerThread, aborting the scan of every other
folder (issue #795).
- Wrap os.scandir so an unreadable/vanished directory is logged and
skipped instead of aborting the whole poll, and widen the per-entry
guard to cover entry.is_dir(), which can also raise PermissionError.
This matches the os.walk default-onerror behavior the watcher relies
on.
- Register the OS/NAS metadata basenames the filters module already
documented but never populated (@eaDir, #recycle, __MACOSX,
Thumbs.db, desktop.ini), so the walker skips the recycle bin entirely
and NAS/OS junk never enters the library.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* update version to 2.1.2
* fix typechecking
* news for v2.1.2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* format
* Squashed commit of the following:
commit 9db6fe273622635700defb5c9015bf540630e40a
Merge: c8984b9ed ed38cb555
Author: AJ Slater <aj@slater.net>
Date: Sat Jul 4 15:57:53 2026 -0700
Merge branch 'develop' into online-estimate-consume-comicbox
commit c8984b9ede16f82f398501adb49c58d5428c168d
Merge: 2b2e63a35 cd84ed99e
Author: AJ Slater <aj@slater.net>
Date: Sat Jul 4 13:22:52 2026 -0700
Merge branch 'develop' into online-estimate-consume-comicbox
commit 2b2e63a35013de0cd5593c8c5cadb360ffbd23ab
Author: AJ Slater <aj@slater.net>
Date: Fri Jul 3 20:36:09 2026 -0700
feat(onlinetag): consume comicbox 4.1.0 estimate; drop the codex copy
Pin comicbox ~=4.1.0 and move the online-tag run estimate onto its
comicbox.online_estimate.estimate_run() home:
- estimate.py becomes a thin seam over comicbox: estimate_seconds()
forwards to estimate_run().seconds and re-exports SOURCE_RATE_PER_MINUTE.
The request/rate constants and math are deleted -- comicbox owns and
tests them now.
- The launcher dialog's per-source rates and per-comic request model derive
from comicbox via a new tagging-estimate.json (choices/onlinetag.py,
build-choices); only display labels stay in the component, so the JS
estimate can no longer drift from the backend.
- The codex estimate test slims to an adapter / re-export guard.
Prep branch: the ~=4.1.0 pin does not resolve until comicbox 4.1.0 is
published, so uv.lock is untouched and CI targets that shell out to `uv`
will fail until then. Post-publish, run `uv lock`; the change was validated
locally with the 4.1.0 modules installed into the venv.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* refactor(onlinetag): extract resume-param sanitizer to cut complexity
AdminOnlineTagResumeView.post crossed radon's C threshold once the resume
descriptor sanitization landed. Move that logic (sources tuple coercion +
dropping keys no task field accepts) into a module-level helper; the view
falls to rank B and reads more directly. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(onlinetag): reference defined constant in match-mode hint
matchModeHint read an undefined COMICVINE_CALLS_BY_MODE, throwing a
ReferenceError on every launcher-dialog render (and failing
tests/unit/launcher-dialog.test.js). Point at the real
TAGGING_ESTIMATE.comicvineRequestsByMode map that callsForSource
already uses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(settings): nest comicbox loglevel/delete_keys under general section
An un-nested overlay is silently ignored by confuse.
* fix(onlinetag): resolve prompts against current DB path, surface apply failures
Answering a deferred prompt fetched the chosen issue against the path
serialized into the prompt at scan time. When an earlier write for the
same comic ran with rename enabled (the comic's other source's prompt,
or a stored-id prefetch), that path was stale and the apply died with an
uncaught FileNotFoundError — after the prompt was already consumed, so
the admin's pick vanished with no feedback.
- _apply_resolution now re-reads the comic's path from the DB by pk;
a missing row reports to the Tagging error panel instead of fetching
a dead path.
- fetch/replay failures (ComicboxError, OSError) and non-resolving
explicit ids now land on the Tagging error panel instead of only the
log, since the pick can no longer be re-prompted.
- stored-id prefetch and tag_by_id also catch OSError so a vanished
file degrades gracefully.
- regression test for the COMICBOX_CONFIG general-section overlay
(un-nested loglevel/delete_keys were silently ignored, letting
comicfn2dict remainders like "(0000)" leak into rename targets).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps and format
* Native OIDC single sign-on (Admin Auth tab) (#798)
* feat(auth): native OIDC login via django-allauth
Codex becomes an OIDC Relying Party (Authentik/Authelia) with a
config-gated login flow:
- [auth.oidc] TOML section + CODEX_AUTH_OIDC_* env overrides
- allauth apps installed unconditionally; behavior gated on
AUTH_OIDC_ENABLED (all OIDC paths 404 when off)
- CodexSocialAccountAdapter: username linking (superusers included,
documented trust boundary), optional email linking, claim-chain
username mapping with sub-hash collision suffix, groups-claim sync
to existing Django groups, admin-group grant/revoke, error
redirects to the SPA (never an allauth template)
- Branded throttled init endpoint /api/v4/auth/oidc/login; allauth
login/callback mounted at /sso/ (outside the namespaced API tree so
allauth's internal reverses work)
- RP-initiated logout URL via cached discovery document using the
spec's client_id parameter (no stored tokens needed)
- /session payload gains public oidcEnabled/oidcProviderName/
oidcLoginUrl and authenticated oidcLogoutUrl
- Profile username locks per-user when an OIDC identity is linked
- OIDC failures reuse the failed-login log line format for fail2ban
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): SSO login button, RP logout, and sso-error page
- auth store: oidc admin flags, loginSSO() full-page navigation,
logout() follows oidcLogoutUrl for RP-initiated logout
- SsoLoginButton shared by the login dialog (with divider) and the
unauthorized lock screen
- /auth/sso-error route + page mapping backend error codes to human
messages, with retry hidden for non-retryable codes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(auth): OIDC setup guide + complete tinyauth forward-auth recipe
- README: native OIDC section (config table, redirect URI with prefix,
Authentik/Authelia walkthroughs, identity-mapping and admin-linking
trust warning, session-lifetime and OPDS caveats)
- README: full nginx auth_request recipe for tinyauth with header
override hardening, Traefik/Caddy equivalents, and a forward-auth
deployment checklist (OPDS + WebSocket gating, spoof test)
- schema test: allauth views stay out of the OpenAPI schema
- test typing fixes surfaced by basedpyright
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(settings): nest comicbox loglevel/delete_keys under general section
An un-nested overlay is silently ignored by confuse.
* fix(onlinetag): resolve prompts against current DB path, surface apply failures
Answering a deferred prompt fetched the chosen issue against the path
serialized into the prompt at scan time. When an earlier write for the
same comic ran with rename enabled (the comic's other source's prompt,
or a stored-id prefetch), that path was stale and the apply died with an
uncaught FileNotFoundError — after the prompt was already consumed, so
the admin's pick vanished with no feedback.
- _apply_resolution now re-reads the comic's path from the DB by pk;
a missing row reports to the Tagging error panel instead of fetching
a dead path.
- fetch/replay failures (ComicboxError, OSError) and non-resolving
explicit ids now land on the Tagging error panel instead of only the
log, since the pick can no longer be re-prompted.
- stored-id prefetch and tag_by_id also catch OSError so a vanished
file degrades gracefully.
- regression test for the COMICBOX_CONFIG general-section overlay
(un-nested loglevel/delete_keys were silently ignored, letting
comicfn2dict remainders like "(0000)" leak into rename targets).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(auth): move OIDC config from codex.toml to the Admin UI Auth tab
OIDCSettings DB singleton (EmailSettings pattern) becomes the sole
config source, read at request time:
- OIDCSettings model + migration 0047 (seeds pk=1, one-time courtesy
import of any pre-GUI [auth.oidc] TOML values); client_secret
encrypted at rest via EncryptedCharField
- get_oidc_settings()/oidc_enabled() in settings.db; cachalot makes
admin edits live on the next request, no restart
- codex/oidc.py rewired to request-time reads; new adapter
list_apps override builds an unsaved SocialApp from the row
(per-app settings['scope'] wins in allauth's get_scope), so
disabled state keeps allauth's own DoesNotExist -> 404 gating
- RP-initiated logout and session flags read the row
- AdminOIDCSettingsView GET/PUT (write-only secret + clientSecretSet
mirror, discovery-cache invalidation on save) and AdminOIDCTestView
(discovery-document probe) at /api/v4/admin/oidc-settings[/test]
- New Admin UI Auth tab mirroring the Email tab: draft/dirty
tracking, never-echoed secret with Clear Credential, redirect-URI
display, Test Connection endpoint report
- [auth.oidc] TOML section and CODEX_AUTH_OIDC_* env overrides
removed; README updated
- Tests now seed the DB row instead of patching module constants
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* format
* update deps and fix
* fix(admin): gate the OIDC enable switch on server URL + client ID
The Auth tab's Enable OIDC Login checkbox is disabled until a valid
server URL and a client ID are entered (it can always be unchecked so
clearing a field never strands the switch). The serializer enforces
the same invariant for API clients and partial updates that blank a
prerequisite while enabled — previously such a save produced a
silently inert enabled=true row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(admin): Auth tab gains Account & Access flags, tinyauth note, name gate
- Move the Account & Access flag cards (Registration, Verify New User
Email, Non-Users) from the Users tab to the Auth tab — they govern
how people get in, which is that tab's subject
- Auth tab prose explains that forward-auth gateways like tinyauth are
not OIDC providers and points them at Remote-User header auth, which
coexists with OIDC
- Provider name joins server URL and client ID as an enable
prerequisite, in the UI switch gate and the serializer invariant
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(admin): visually nest the OIDC subsections under their header
AdminSection gains a sub variant: a small uppercase overline title (h4,
$text-meta) and an indented left rule, with tighter sibling rhythm than
top-level sections. The Auth tab wraps the whole OIDC block — prose,
Identity Provider, User Mapping, Logout, and Test Connection — in one
parent 'OIDC Single Sign-On' AdminSection with the config groups as sub
sections, so their subordination to the OIDC header is unmistakable
next to the sibling Account & Access section. Documented in DESIGN.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(admin): collapse the OIDC section when OIDC is disabled
Most admins never configure OIDC, so the section body — prose, config
sub-sections, and Test Connection — hides behind an AdminExpandToggle
disclosure. It starts expanded only when OIDC is already enabled;
otherwise a one-line hint summarizes what's inside next to a Configure
toggle. The disclosure is initialized once from the saved state so
saving a disable doesn't slam the panel shut mid-edit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(admin): plain-English hints for PKCE and other OIDC jargon fields
PKCE, Client ID, Username Claim, and Groups Claim now carry hints an
admin who has never touched OIDC can act on — including what a claim
is and why PKCE should stay on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): authentik + tinyauth manual test harness in test-proxy/
Adds a docker-compose IdP stack and nginx wiring so SSO can be manually
verified before release:
- compose.yaml: authentik (OIDC provider, :9010) + tinyauth (forward
auth, :3232), everything bound to localhost with throwaway creds
- authentik/blueprints/codex-test.yaml: auto-applied fixtures — readers
and codex-admins groups, testuser/testadmin, and the codex-test OIDC
client with callback URIs for proxied and direct, prefixed and bare
- forwardauth.conf: nginx :8081 gating Codex behind tinyauth
auth_request with an overriding Remote-User header
- README.md: step-by-step test matrix covering native OIDC (login,
group sync, admin mapping, RP logout, linking, error page, disabled
404) and forward-auth (login, gating, spoof-proofing, coexistence)
tinyauth DB path pinned to the writable /data volume (workdir is
root-owned). test-proxy/ excluded from eslint: authentik !Find tags and
compose healthcheck arrays require flow-style YAML the yml plugin bans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): run the harness nginx as a compose service
nginx joins authentik + tinyauth in compose.yaml, so only Codex runs on
the host. The native bin/run-test-proxy.sh path still works — both share
server.conf/forwardauth.conf, with the sole native-vs-container
difference (backend addresses) isolated into named upstreams:
- upstreams-native.conf: localhost backends (host nginx)
- upstreams-docker.conf: host.docker.internal + tinyauth service name
- connection-upgrade.conf: the ws-upgrade map, now shared
- ssl-listen.conf / ssl-listen-none.conf: SSL/QUIC listeners split out so
the container serves plain HTTP (native keeps the 8443 listeners)
The compose nginx mounts these into the stock image's conf.d and reaches
host Codex via host.docker.internal (extra_hosts host-gateway for Linux).
Also fixes a latent harness bug that would break OIDC through the proxy:
X-Forwarded-Host used $host (strips the port), so Django's
build_absolute_uri produced a portless redirect_uri that couldn't match
the registered callback. Now $http_host, port included.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): fix tinyauth boot crash on localhost app URL
tinyauth v5 derives a cookie domain from its app URL at startup and
rejects single-label hosts and IPs ('invalid app url, must be at least
second level domain'), so http://localhost:3232 crash-looped. Move the
forward-auth path onto *.localtest.me (all subdomains resolve to
127.0.0.1 via public DNS, every browser, no /etc/hosts):
- tinyauth app url -> http://tinyauth.localtest.me:3232
- gated Codex -> http://codex.localtest.me:8081
- shared cookie -> .localtest.me (spans both)
The @tinyauth_login redirect and README Test 2 follow. OIDC/authentik
stay on localhost (no cross-host cookie needed there).
Also documents in README Troubleshooting that the harness publishes only
9010/8080/8081/3232 and never binds Vite's 5173 — a blocked HMR is a
stale vite process, and 8080/8081 clashes come from running native
make dev-reverse-proxy alongside the compose nginx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): set authentik provider grant_types; drop IPv6 host-gateway
Two issues from the first live OIDC run:
- 'Login with Authentik' failed with authentik logging 'Invalid
grant_type for provider'. authentik 2026.x added an explicit
grant_types model field that defaults to an EMPTY list, so a blueprint
that omits it creates a provider allowing no grants and the authorize
step returns invalid_request. Set grant_types: [authorization_code,
refresh_token] on the provider.
- nginx logged 'connect() to [fd..::254]:9810 Network unreachable' then
fell back to IPv4. The IPv6 came from extra_hosts host-gateway (a
Docker Desktop IPv6 ULA gateway Granian doesn't listen on). Comment it
out — Docker Desktop provides an IPv4 host.docker.internal built-in;
Linux users uncomment it.
README troubleshooting covers both, including re-applying the blueprint
to an already-running authentik.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auth): refresh public flags when OIDC is toggled or on logout
The 'Login with <provider>' button (adminFlags.oidcEnabled) went stale
after disabling OIDC: OIDCSettings is a singleton with no
admin.flags.changed websocket broadcast, and logout() left adminFlags
untouched, so the button lingered on the login screen until a manual
page reload.
- admin.updateOidcSettings now calls auth.loadAdminFlags after a save,
resyncing the public OIDC flags immediately.
- auth.logout now re-fetches public flags (except when doing an
RP-initiated full-page redirect, which reloads anyway), so the
logged-out login screen always reflects current settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* chore(lint): clear radon CC/MI and remark warnings
- codex/oidc.py: extract CodexSocialAccountAdapter._sync_admin from
_sync_user (rank C -> B); keyword-only bool arg for FBT001.
- tests: split the 940-line test_onlinetag_session_manager (MI rank B,
pre-existing on develop) — move the TagPassRunner and stored-id-map
classes into test_onlinetag_tag_pass.py, importing the shared doubles
from the session-manager module (as test_opds_schema already does).
Both files now MI rank A.
- test-proxy/README.md: wrap the bare http://localhost autolink in <>
and the [fd..::254] nginx error in backticks so remark-lint stops
reading it as a link reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* bump version 2.2.0
* bump news
* update deps and comicbox
* adapt ComicVine credential check to simyan v3 (comicbox 4.1.1)
simyan 3.0 removed the cache= constructor kwarg. The credential check
now passes cache_expiry=DO_NOT_CACHE with the cache/ratelimit sqlite
files in a throwaway temp dir, so validation always hits the network
(api_key is excluded from simyan's cache key) and leaves no files
behind. Also note comicbox 4.1.1's ComicVine improvements in the
v2.2.0 news.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* test(onlinetag): assert merge flag is forwarded, not comicbox's arithmetic
test_estimate_seconds_passes_merge_flag pinned a hardcoded 1000.0 that
went stale when comicbox 4.1.1 changed Comic Vine pacing to bill the
busiest resource pool (simyan 3.x per-endpoint buckets) instead of the
request total. The codex seam only forwards to comicbox.estimate_run, so
re-derive nothing here: assert the flag's defining effect — merge sums
every source's pace, so it costs strictly more than first-match-wins —
which proves forwarding without coupling to comicbox's rate model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
comicbox 4.3.0 (mokkari 4 header-driven Metron rate limits), pinia 4.0.2,
vue-router 5.2, vuetify 4.1.5, vite 8.1.5, eslint plugin updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v2.2.1: show Metron account rate limits live (comicbox 4.3.0 / mokkari 4)
Testing Metron credentials in the Admin panel now reports the account's
real burst and daily limits read off the validation response's
X-RateLimit-* headers — the daily limit reflects the user's Metron donor
tier. The online tagging status table shows the live daily budget as a
run progresses, via comicbox 4.3.0's newly wired
OnlineSession.rate_limit_status().
Also removes a ty ignore in tests/opds_schema.py made stale by the dep
updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* v2.2.1: community ratings replace critical rating (comicbox 4.4.0)
comicbox 4.4.0 remaps ComicInfo CommunityRating / CBI rating — the tags
that fed critical_rating — to its new community_rating field
(average_rating + rating_count, Metron-filled), and critical_rating no
longer persists to any format. Migration 0048 renames the column
(values carry: same tags, same scale), adds community_rating_count and
alternative_issue number/suffix columns, and remaps user settings that
reference the old key (order_by, table_columns JSON) in RunPython.
Community rating gets full browser parity: sort (Avg aggregate), table
column, sidebar filter, and field search incl. rating_count. The
metadata dialog shows '4.2 / 5 (128 ratings)' and the tag editor edits
the pair, with the count enabled for MetronInfo only (the only format
that persists it). Alternative issues import and display ('#43.5AU').
Sidecar backups tolerate the rename both ways: schema column renamed,
restore remaps legacy critical_rating filter columns, order_by values,
and table_columns keys from old dumps.
Also: ty ignores for dep-bump invalid-method-override errors in
vuetify serializer fields; test fixtures rebuilt with deterministic
past-dated zip mtimes (the importer prefilter skips future-dated
embedded mtimes as unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* v2.2.2: fix rotated pdf page serving (comicbox-pdffile 0.6.3)
PDF pages scanned upside down or sideways and righted by the pdf's
rotation attribute displayed rotated when the reader served them as
images; pdffile 0.6.3 re-renders rotated image-dominant pages instead
of serving the stored bytes. Also stops a read-only page serve from
rewriting a pdf on disk when MuPDF repairs its content streams in
memory (close() no longer saves on the repair-dirty flag).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps, including comicbox 4.5.0
* Fix silent no-op when clearing tag editor fields
Clearing a field encoded the clear as an empty patch value (""/{}/null),
but comicbox prunes empty values on schema load and a merge write can
only add or replace — so every "clear field" action was a silent no-op
on the archive.
Cleared and emptied fields now travel as comicbox delete_keys glom paths
(new in comicbox 4.5.0): buildPatch returns {patch, deleteKeys} ->
tag-write and preflight POSTs -> serializer -> BulkTagWriteTask ->
BulkWriteItem. A clear-only edit sends an empty patch and still writes.
Rename previews layer the delete keys onto the preview config so a
cleared series or issue drops out of the previewed filename too.
Also document why the read-side COMICBOX_CONFIG must never become the
write base config: comicbox unions a write's delete_keys with the base
config's, and the read config skips every schema field codex doesn't
consume, so using it would strip all of them from the user's archive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix lint errors from the ruff 0.16 upgrade
The dependency update to ruff 0.16.0 enabled rules that flag four
pre-existing spots, failing lint (and CI) independently of any code
change: RUF036 None-last in two exception handler unions, PLC0206 dict
iteration without .items(), and PLR0917 too many positional arguments.
The mail backend's positional signature mirrors Django's
SMTPBackend.__init__ to stay a drop-in, so PLR0917 joins the PLR0913
suppression already there rather than changing the signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update comicbox
* Clear monochrome by deleting the tag, not writing false
comicbox 4.5.1 fixes ComicInfo's BlackAndWhite in both directions, so
monochrome is now a real tri-state tag: Yes, No, or absent. The clear
icon set the patch value to false, which now writes <BlackAndWhite>No
</BlackAndWhite> — asserting the comic is known to be color instead of
removing the tag. The bug was invisible before 4.5.1 because nothing
was written at all.
Cleared monochrome joins the other cleared fields in delete_keys;
explicitly unchecking the box still patches a false.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update devenv and deps
* update claude rules about telemetry
* Fix anonymous stats sending and report what codex grew into
v2.2.3. Two problems: stats had not been sent by any install since v2.0.0,
and what they would have sent no longer described codex.
Transport. Replacing requests with urllib left the server credentials in the
URL. urllib hands the whole netloc to http.client, which reads the password
as a port and raises InvalidURL, so every send failed before opening a
socket. A leftover requests-style raise_for_status() would have broken a
successful send too. Both were logged at debug, which is why this went
unnoticed for months; failures now log at warning. Credentials move to an
Authorization header, and CODEX_TELEMETER_URL points a dev install at a local
chronicle.
Payload, now wire version 2. Adds counts and settings for everything since
v1.12: read-only libraries, custom covers, favorites, bookmarks, community
ratings, browser table view, online tagging, single sign on, email, rate
limits and reverse-proxy deployment. Identifiers are counted by source and
type, which is the only durable measure of online tagging use.
It stays counts, booleans and closed-enum values, per the telemetry privacy
rule. Nothing an administrator typed is included: no paths, account or group
names, search terms, banner text, service URLs, API keys or credentials. A
setting holding text or a secret reports only whether it is set. Identifier
source names come from comic files, so they are mapped through comicbox's
known sources and anything else becomes "other" before it leaves the process.
tests/test_telemeter_privacy.py seeds a sentinel into every such field and
fails if one reaches the payload, then requires every value to be a number, a
boolean, or a string from a closed vocabulary.
The Admin Stats tab renders every new section, so the page still shows the
whole report. Two label fixes there: the metadata table pluralizes by
appending "s", which produced "Storys" and "Comic Metadata Importeds".
Also: the identifiers section never reached the admin endpoint.
AdminStatsRequestSerializer fills in every section it declares, so params is
always truthy and the section gate drops anything undeclared - an empty table
with no error. Declared it, and added a test so the next section added to
StatsSerializer cannot vanish the same way. FILE_TYPES_CHOICES was missing
CB7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* Support comicbox 4.6.0 series alternative names
comicbox 4.6.0 records a series' localized and variant titles as
reprints: from MetronInfo AlternativeNames and Reprints tags, and now
from Metron and Comic Vine online tagging. Codex dropped the field
entirely -- reprints sat outside USED_COMICBOX_FIELDS, so the read
config told comicbox to skip parsing it and the aggregator popped it as
a backstop.
Import them into a new denormalized Reprint model keyed on series name,
volume number, issue and language. Series and Volume are browse
collections, so reusing them would have hung phantom rows off the
browser for every alternate title. A reprint may carry only a series
sort_name, which stands in when the name is absent; one with neither is
dropped.
Alternate names reach the metadata panel, the tag editor, a sortable
browser column and filter, and a distinct alternate_series FTS column,
so a comic filed under a localized title is findable by it. The column
is distinct rather than folded into series to keep the series: token
exact; unqualified searches match either.
Online matching now recognizes Comic Vine volume aliases, which codex
inherits by delegating matching to comicbox. The match prompt shows
each candidate's alternative names, since a comic filed under a
localized title matching its canonical volume otherwise reads as a
wrong match, and passes the chosen candidate's volume id, which was
plumbed end to end but always null.
Drop the auto_threshold setting. It was parsed, serialized and
round-tripped through resume, but never reached OnlineSession, which
accepts no threshold.
Reconcile sidecar columns on open. schema.sql is all CREATE TABLE IF
NOT EXISTS, so a sidecar predating a release never gained its new
settings_filters columns and the next dump failed with "no such
column". The wanted columns derive from the schema file rather than a
list that would rot the same way. This also repairs the column the
community_rating rename added.
Already-imported comics pick alternate names up on a Force Update Tags;
adding a field to the whitelist does not invalidate metadata_mtime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps and devenv
* bump news for comicbox 4.6.1
* update devenv
* fix cron double enqueue of telemeter task
* test crond double enqueu fix
* test telmemeter logging fixes
* feat(api): reintroduce Swagger UI at /api/v4/
The interactive docs were lost in the v4 cutover (9b8314d3b, v2.0.0)
when codex/urls/api/v3.py was deleted; only the raw schema route
survived. Mount SpectacularSwaggerSplitView at the v4 root, gated on
FEATURES.swagger — the flag that until now only switched a CSP overlay
for routes that no longer existed.
The split view keeps its init javascript in a second same-origin
request, so no inline <script> needs a nonce.
Also fix the CSP overlay it depends on: it listed the jsdelivr bundles
under script-src only, but the pdfs-dist overlay declares
script-src-elem, which masks the script-src fallback for element loads
and would have blocked both bundles. List them under both directives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* v2.2.4
* sub api v3 for v4
* feat(tagging): authenticate to Metron with an API key
Metron issues API tokens now, so the admin Tagging tab offers a single
API Key field and no longer mentions usernames or passwords. The key
lands in a new encrypted metron_key column (migration 0050) and reaches
comicbox as OnlineCredentials.metron_key, which mokkari uses as its
Bearer token. Preferring the key over a login needs no code: mokkari
drops basic auth whenever a token is present.
Logins saved before this release keep working. Every path that decides
whether Metron is configured -- the scan session, tag-by-id, credential
testing, and the telemetry boolean -- accepts a key or a username and
password pair, matching comicbox's own is_configured. The validator
passes api_token=None rather than "" so an absent key doesn't send an
empty Bearer header and defeat that fallback.
Writing the key retires the login it replaces: a PUT carrying
metron_key blanks metron_user and metron_password, so saving a key or
clearing credentials both leave no stale login behind. A save that
omits the field (a custom-URL edit, the settings auto-save) leaves a
stored login alone.
The user-data sidecar exports metron_key too, and restore skips
coalesced columns a backup predates -- sqlite3.Row raises on a missing
column, so an older sidecar would otherwise crash the restore.
Requires comicbox 4.7.1, which also warns once per process when basic
auth is what actually gets used.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* feat(tagging): remove the custom URL fields for Metron & Comic Vine
The Metron custom URL never did anything: mokkari hardcodes
METRON_URL = "https://metron.cloud/api/{}/" and exposes only dev_mode as
an alternative, so comicbox's metron source drops the credential url and
warns that it is a no-op. The Comic Vine override did work via simyan's
base_url, but a comic server has no use for pointing at a different
Comic Vine.
Removes both fields from the model (migration 0051), the admin and
validate serializers, the validate view's credential tuple, all three
librarian consumers, the two telemetry booleans and their stats
serializer fields, and the user_data backup/restore path. comicbox keeps
its OnlineCredentials url fields; codex just stops passing them, so
test_online_credentials_fields_stable still expects them.
Old sidecar backups carrying the dropped columns restore fine: the
restore comprehension walks its own allowlist rather than the row, the
same way it already ignores the retired active_session_id.
Also fixes a bug from 8fbf7e11f in the same DDL: schema.sql never gained
a metron_key column, so the tagging_defaults upsert failed with "no such
column" — and since _dump_queryset only logs per-row failures, every
backup silently wrote zero tagging rows. Adds the column (_reconcile_
columns retrofits existing sidecars) and a dump round-trip test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): clear the two outstanding ty errors
test_snapshot_diff: a real annotation bug. The ``_snapshot`` helper took
``models: dict[str, type]`` but assigns it straight to
``Snapshot._path_to_model``, which is ``dict[str, type[Model]]``. All
five call sites already pass Comic or Folder, so the parameter was just
looser than both its callers and its destination.
test_bookmark_filter_isolation: unavoidable suppression. The mixin
declares ``self.request: Request`` and the helper deliberately assigns a
SimpleNamespace, since its whole point is to skip DRF's request
lifecycle. A ``cast`` traded the ty error for basedpyright's
reportInvalidCast (the types don't overlap), and its suggested
double-cast through ``object`` reads worse without making the stub any
more of a Request. Adds ``# ty: ignore[invalid-assignment]`` beside the
existing pyright ignore, per the two-checker convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tagging): present Metron as an API key source & warn on legacy logins
The admin Tagging tab called Metron's credential a "credential" while Comic
Vine's twin panel called it an API key. Metron authenticates with a
metron.cloud API key now, so say so — except on an install whose only stored
credential is still the legacy username & password.
- Metron's status, clear button, and confirm dialog switch between an API-key
and a legacy-credential label set, keyed on metronKeySet.
- A legacy-only install gets a warning-colored deprecation notice linking to
Metron's token authentication announcement, and the panel opens itself so
the notice isn't buried behind a click.
- The Online Tagging dialog carries a one-line version of the same warning,
shown only when a legacy login is stored AND this session would query
Metron (selected on the Search tab, or the id's source on the By ID tab).
- Comic Vine's save button and the source-disabled tooltip say API key too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update devenv
* update deps
* fix(importer): import a renamed comic's new tags instead of failing
Tagging a comic with rename enabled produced a failed import for the
pre-rename path, and the tags that had just been written never imported
at all.
The tag write and the rename land in one watch batch: a modify naming
the old path, plus a delete+add that inode matching pairs into a move.
Move detection never looked at the modify, and the task builder pruned
modified paths only against move destinations, so the task carried
files_moved={old: new} alongside files_modified={old}. The importer
applies moves before reading, so the read opened a path that no longer
existed.
Remap modified paths through the move map rather than dropping
destinations. Sources become their destination (the write-then-rename
every external tagger performs, codex's own included), and destinations
survive, which the poller emits deliberately for a move whose stats
also changed.
Stop the move phase from refreshing Comic.stat. The stored stat means
"the file as of its last tag import", so refreshing it on a move erased
the only evidence the read phase had that the renamed file's contents
had changed too -- the tags were lost rather than deferred. A pure
rename leaves inode, mtime and size alone, so the preserved stat still
matches disk.
Also stop recording failed imports for files that vanished mid-import:
the row was queued before presave() stat'd the path, so the OSError
meant to drop it did not. Key the failed-import map by str so its
membership test against db paths can match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v2.2.5
* feat(tagging): combine online search and tag-by-id into one pass
The Tag Online dialog's Search and By ID tabs become one pane. Ids are
pinned per source: a pinned source is fetched by that issue id while the
unpinned ones search, in a single comicbox lookup, so merge_all_sources
merges across both. The submit button reads Search, Tag by ID & Search,
or Tag by ID accordingly, and entering an id selects its source.
Ids now ride on tag-sessions/start as {source: token}. That retires the
parallel POST /admin/tag-by-id path entirely -- AdminTagByIdView,
OnlineTagByIdTask, TagByIdRequestSerializer -- so tagging by id gains
session status, resume, and the write pipeline the scan already had.
Also drops dry_run, which the start view accepted and never read.
run_session skips the DB stored-id prepass when ids are pinned: the
prepass pops the comic out of comic_paths, which would leave the
unpinned sources nothing to search.
Needs comicbox 4.8.0 for OnlineSession(ids=...). That release is not on
PyPI yet, so the pin here is still ~=4.7.1 -- bump it after publishing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* style(news): prettier wrap the online tagging entry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tagging): restore the custom URL for Comic Vine
v2.2.4 (0dd3e5199) removed the custom URL fields for both online
sources. Metron's was correctly retired — mokkari hardcodes
METRON_URL and comicbox warns the url is a no-op — but Comic Vine's
worked: comicbox passes it through OnlineCredentials.comicvine_url to
OnlineSourceCredentials.url and on to simyan's base_url, which is what
lets tagging run against a Comic Vine proxy or mirror.
Restores the Comic Vine half only, no comicbox change needed:
- Model field + migration 0052. A plain URLField rather than an
EncryptedCharField, because unlike the API keys it is not a secret
and must read back for the admin form's placeholder.
- Admin serializer (read+write), validate request serializer, and the
validate view's _CREDENTIAL_FIELDS.
- All three librarian consumers: the scan session's OnlineCredentials,
the explicit-id auth mapping, and the credential validator's simyan
base_url, so Test checks the endpoint the scan will actually use.
- Backup/restore sidecar column, serializer, and restore allowlist.
_reconcile_columns retrofits existing sidecar files; a legacy
metron_url column in an older backup stays silently ignored.
- Telemetry reports only bool(comicvine_url) — never the value.
- The admin Tagging tab's Comic Vine panel gets the field back, its
save button reverts to "Save Comic Vine Credentials" now that a
URL-only save is possible again, and Clear removes both.
A URL alone is not a credential: it must not satisfy the session
manager's configured-sources gate, enable the source checkbox, or pass
validation without a key. Tests pin all three.
URLs dropped by 0051 are unrecoverable; admins re-enter them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* bump version and update deps
* bump dockerfile source version
* granian 2.8.0
* update deps
* update devenv
* update devenv and deps
* use nodejs26 in the builder
* update deps
* remove old ty ignore
* update deps
* fix radon complexity warnings
Three items `make complexity` flagged, all pure refactors.
AdminOnlineTagStartView.post (CC 11 -> C): three near-identical
"request value or stored default" blocks collapse into a
_FLAG_DEFAULT_FIELDS mapping plus a _resolve_flags helper.
test_telemeter_privacy._vocabularies (CC 12 -> C): split by vocabulary
source into _choice_vocabularies, _identifier_bucket_vocabulary and
_tagging_vocabularies, with the literal and OIDC sets hoisted to module
constants. The same string set is checked; the walk is not widened.
test_onlinetag_session_manager (MI 17.37 -> B): 916 lines in one module,
every function already rank A. Split along its seams into the scan pass,
prompt resolution and credentials, with the doubles and the comic factory
moved to tests/onlinetag_session_fakes.py. All 31 tests come across
unchanged; the move dedupes the BulkTagWriteTask filter into
write_tasks() and the prompt dict into a _prompt() factory.
test_onlinetag_tag_pass now imports the fakes from their new home.
make complexity, lint, ty clean; 815 pytest + 371 vitest pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update deps
* fix(importer): skip comic moves onto claimed paths
A move whose destination path already belonged to another comic row
violated the (library, path) unique constraint inside bulk_update. The
IntegrityError aborted the whole import before the delete phase could
clear the stale row, so the poller rebuilt the same task and crashed on
every subsequent scan.
Mirror the folder guard for comics and custom covers: drop moves onto
paths an existing row holds, and moves that two sources in one batch
claim, which files_moved can express because it is a plain dict rather
than a bidict. Skipping converges. The destination row's stat refreshes,
the next scan sees two rows sharing an inode and suppresses the bogus
move, and the stale source falls through to the delete phase.
Also contain any move phase failure in all three move steps so a bad
batch degrades to a skipped phase the next scan reconciles instead of
aborting the import. This covers the same class of crash in the folder
step, where converting dirs_moved to a bidict raises on the duplicate
destinations the watcher can emit.
Fixes #807
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* bump version
* update deps
* update deps
* fix(update): make codex self-update work on macos and linux
The self-update was not flaky, it was inert.
Nothing ever asked for an automatic update. update_latest_version()
had an `update` hook that queues JanitorCodexUpdateTask and no caller
passed it, and the update task was not in the nightly fan-out, so the
Auto Update flag promised a daily upgrade that could never happen. The
nightly check now forces a fetch and chains into the update when the
flag is on. The flag gate moves to that scheduling point, so the admin
Jobs tab button updates whether or not the flag is set -- it used to
silently no-op, since the flag defaults off and the button sends
force=False.
The installer ran `sys.executable -m pip install --upgrade codex` with
no timeout, no captured output, and a blanket except that swallowed
every failure while still logging "updated to the same version". uv and
pipx environments have no pip and the docker image uninstalls it, so
those installs always failed invisibly. Now: pip if importable, else
`uv pip install --python <sys.executable>`, else an ERROR naming both
and pointing docker at a new image. Failures log the installer's own
stderr, a hung installer times out instead of wedging the scribe queue
behind it, and the restart only fires when the install really happened.
Restarting exec'd __file__, which relied on run.py's executable bit and
its `#!/usr/bin/env python3` shebang resolving to a python that has
codex installed. Under macOS, pipx, uv, systemd and launchd that is
frequently a different interpreter and the server never came back. Exec
`sys.executable -m codex.run` instead.
_is_outdated() called Version() on unvalidated strings, raising on a
fresh install's empty cache and on a source checkout's "test" version.
Version comparison is now codex.version.is_outdated(), total by
construction, shared by the janitor and the version view.
The browser could not announce anything: semverGreaterThan(a > b) passed
one boolean into a two-argument function so `outdated` was always false,
and the comparison read 1.0.9 as newer than 1.1.0. The server ships
`outdated` and `docker` in the version payload and the javascript
comparison is gone. The footer renders a codex orange "upgrade to codex
vX.Y.Z" link to the Update Codex job, or to the image repo in docker,
where codex cannot install over itself. The Jobs tab grows section
anchors, names the version the job would install, and explains the
docker case.
While the cache is empty every /api/v4/version request queued a fetch
task, each on its own thread with a 5 second PyPI call. Add a process
wide in-flight lock and a cooldown after a failed fetch.
Tests cover the version comparison, the payload, installer selection and
failure paths, the flag gate and fetch guards, the nightly wiring, and
both frontend surfaces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(proxy): document proxy_pass_header Server for nginx
nginx hides the upstream Server header by default (along with Date,
X-Pad and X-Accel-*) and substitutes its own, so codex/<version> never
reaches clients behind a reverse proxy. Granian passes the header set by
CodexMiddleware through untouched; nginx is the only clobberer.
Add proxy_pass_header Server to the README's example location, plus a
subsection covering the curl verification, why server_tokens off is not
a substitute, and the array-directive inheritance trap (a location that
declares any proxy_pass_header drops the one inherited from server{}).
Set the same directive in the test-proxy harness so the documented
config is the one actually exercised.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update new for docs too
* update deps
* update deps
* fix(opds): vary the response cache on User-Agent (#813)
OPDS responses were cached without varying on User-Agent while the body
depends on it, so a client could be served a variant rendered for a
different one until the entry expired. UserAgentNames switches facet
emission (FACET_SUPPORT), download mime types (SIMPLE_DOWNLOAD_MIME_TYPES),
order facet suppression (CLIENT_REORDERS) and absolute hrefs
(REQUIRE_ABSOLUTE_URL), and cache_page keys only on the URL plus the
headers named in Vary.
Add User-Agent to the existing vary_on_headers in opds_cached, which
covers every wrapped v1 and v2 feed, start, manifest and opensearch
route. Vary already includes Cookie, so per-session keys existed anyway
and this barely fragments the cache further; it also corrects what
intermediary caches are told. Cover routes keep the narrower vary since
covers don't depend on the client, and the static authentication
document is left alone.
Adds tests/test_opds_cache.py asserting the Vary header names User-Agent
and that a feed primed by one client isn't replayed to another. Both
fail without the fix.
Fixes #811
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): send v1 entry contributors, which were silently dropped (#814)
OPDS1TemplateEntrySerializer declared the field as `credits`, but the
entry object exposes `contributors` and the template iterates
`entry.contributors`. The template renders serialized data, so the
mismatch meant DRF looked for a `credits` attribute on the entry, found
none, and skipped the field: `credits` is read_only and not required, so
get_attribute raises SkipField and the key is omitted with no error.
`contributors` was never declared and so never serialized, leaving the
template's contributor loop iterating nothing.
The result is that comic credits which are not writing credits -- artists,
colorists, letterers, everyone outside AUTHOR_ROLES -- have never appeared
in a v1 feed. `<author>` was unaffected because all three layers agree on
that name. Atom allows zero or more `atom:contributor` in an entry, so the
schema tests could not catch it either.
Rename the field to match the entry property and the template. The payload
shape was already correct: get_credit_people and its batched variant return
objects with .name and .url, exactly what OPDS1CreditSerializer expects.
Adds tests/test_opds_contributors.py, which seeds a Writer and a Colorist
and asserts each lands in its own element. Without the rename the author
assertion still passes and the contributor one fails, which is the shape
of the bug.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): recognize Panels as a facet capable client (#812)
Panels on iOS added OPDS facet and sort support in 3.13.0, so add it to
UserAgentNames.FACET_SUPPORT. Its CFNetwork style UA parses to "Panels"
through get_user_agent_name, so the existing exact-name match works.
Two more fixes came out of testing that allowlist.
Facet capable clients received the facets twice. The gate in the v1 feed
`entries` property had been commented out, so facets() ran unconditionally
and its OPDS1Link objects were appended to the entries list alongside the
real facet links from _links_facets. OPDS1TemplateEntrySerializer drops
every field those links don't have, so each one rendered as a dead entry:
empty id, no links, unclickable. Restore the gate so the fake navigation
folders are emitted only for clients that can't read facets.
The opds:facetGroup attribute carried internal query parameter names, and
clients like Panels show them verbatim as filter menu headings. Add a
display_name to the FacetGroup dataclass and emit that instead, so the
headings read "Order By", "Order Direction" and "Views". The query param
still drives hrefs and active-facet detection. facet_group also becomes a
CharField; it was typed as a collection-name ChoiceField whose choices
never included any value actually assigned to it.
Adds tests/test_opds_user_agent.py covering both facet variants, the
display names, and User-Agent parsing. Each test fails without its
corresponding fix.
Fixes #810
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* bump version to 2.2.9
* update deps
* memoize use facets order
* fix(opds): gate Panels facet support on build number (#815)
Panels' facet support is per platform and the platforms share one UA
name: the macOS build (951) does not render OPDS facets while iOS builds
(952 and later) do. Matching FACET_SUPPORT on the name alone sent macOS
Panels facet links it can't render, and with no fake nav folders either
it had no sort UI at all.
get_user_agent_name now also returns a build number, parsed from the
token right after the first slash for clients listed in _BUILD_UA_NAMES
(Panels only today). The auth mixin memoizes the pair and exposes it as
user_agent_name and user_agent_build, so existing name consumers are
unchanged. use_facets requires the client's build to meet
UserAgentNames.FACET_SUPPORT_MIN_BUILD (Panels: 952) in addition to name
membership; a missing or unparseable build fails the floor, falling back
to the fake nav folder sort that works on every client. Clients without
a floor, like kybooks, are unaffected.
No cache change needed: the response cache already varies on the full
User-Agent header, so builds 951 and 952 key separately.
Updates tests/test_opds_user_agent.py: the iOS constant moves to build
952, a new test pins that build 951 keeps the nav folder sort and gets
no facet links (it fails against the name-only gate), and the parse unit
tests cover the (name, build) pair including unparseable builds.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* Migrate deprecated EMAIL_* settings to Django's MAILERS (#816)
Django 6.1 deprecates the discrete EMAIL_* connection settings
(RemovedInDjango70Warning at django.setup()), and defining MAILERS makes
reading the old names an AttributeError.
- Replace the eight deprecated settings with an EMAIL_CONNECTION_OPTIONS
dict (TOML/env layer, keyed by EmailBackend constructor kwarg) plus a
MAILERS declaration pointing at the DB-aware DBEmailBackend.
- get_email_connection_kwargs / get_email_from_address coalesce the
EmailSettings DB row over EMAIL_CONNECTION_OPTIONS instead of the
removed settings.
- DBEmailBackend defaults its mailer alias so direct construction never
hits the SMTP parent's pre-MAILERS settings fallback.
- The admin test-send view builds the backend directly and calls
send_messages(), dropping deprecated get_connection() and
EmailMessage(connection=...).
- Tests override MAILERS + EMAIL_CONNECTION_OPTIONS instead of
EMAIL_BACKEND/EMAIL_HOST.
Claude-Session: https://claude.ai/code/session_01Bqa2ULBSaSVDwDSMEni1hf
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): denylist facet-blind Panels builds instead of a build floor (#817)
The build >= 952 floor was built on a wrong premise: Panels build
numbers interleave across platforms. Real iOS builds run lower than the
macOS build - 942 (reported in the field) and 950 (issue #810's
reporter) both render facets natively, while macOS 951 does not - so no
floor can separate them, and 952 shut real iOS users out of facets,
handing them the fake nav folder sort instead.
Replace FACET_SUPPORT_MIN_BUILD with FACET_BLIND_BUILDS, a per-client
frozenset of known facet-blind builds (Panels: {951}). use_facets now
refuses only those builds; every other build - unknown and unparseable
ones included - gets facets, which is the pre-gate behavior that worked
on iOS. A future facet-blind macOS build must be added to the set as
discovered; until then it receives facets it ignores, the pre-gate
status quo.
The iOS test constant moves to the field-reported build 942, an
unparseable-build UA joins the facet-capable cases to pin the
facets-by-default behavior, and the macOS 951 test still asserts the
nav folder fallback. The 942 and unparseable cases fail under the old
floor gate and pass under the denylist.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* update deps
* update deps
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ajslater
added a commit
that referenced
this pull request
Aug 24, 2026
* fix(frontend): send passwordConfirm from profile change-password (#793)
The Profile dialog's self-service password change posted only
oldPassword + password to /api/v4/auth/password/change, but that
endpoint is rest_registration's ChangePasswordView whose serializer
requires password_confirm (camelCased passwordConfirm) — so the
request 400'd with "passwordConfirm field is required".
The dialog already collects and validates passwordConfirm; forward it
in the changePassword payload, matching change-password-dialog.vue and
the register/reset flows. Add a regression test asserting the field is
sent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps and version to v2.1.1. bump news
* test(frontend): work around vitest/valid-expect false positive on expect.any
The @vitest/eslint-plugin valid-expect rule misclassifies expect.any() as
chai's `.any` flag chain and reports "unknown modifier". Disable the rule on
the one nested assertion with a documented reason rather than weakening it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(onlinetag): drop dead effort option; model Metron's fewer requests
comicbox 4.0.5 no longer applies the effort knob to Metron tagging, and
Metron's search is now a flat two-step (series_list + issues_list) that
match mode does not change.
- Remove the vestigial `effort` option (serializer, task, resume params,
and test). It was collected by the API but never passed to comicbox's
OnlineSession.
- Count estimate calls-per-comic per source: Metron a flat 2, Comic Vine
keeps its per-mode 2/3/5. First-match-wins bills the costliest single
source; merge sums per-source calls. Mirrored in the launcher dialog.
- Resume view drops unknown persisted params so a pre-upgrade `effort`
key in the file-based cache can't crash the task rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* trim news
* refactor(onlinetag): derive source list + issue-id parser from comicbox
Two facilities codex hand-synced from comicbox now consume it directly:
- Source names: KNOWN_SOURCES and the task/serializer/frontend default
lists derive from comicbox's canonical SOURCE_NAMES tuple instead of
repeating {"metron","comicvine"} literals in four places. The frontend
gets it through the tagging choices JSON (build-choices), so a new
comicbox source propagates without hand-editing every site.
- Issue-id parsing: the two byte-identical trailing-int regex copies
(stored_id_prepass, explicit_id) collapse into one
issue_id.parse_issue_id built on comicbox's canonical PARSE_COMICVINE_RE.
It honors the real Comic Vine 4-digit long-key rule instead of grabbing
any trailing int; an unrecognized key returns None, which safely falls
back to search / rejects the id rather than guessing wrong.
No user-visible behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(onlinetag): scope the match-mode request-count hint to Comic Vine
The "~N requests/comic" tail in the match-mode hints only describes Comic
Vine, whose calls scale with match mode; Metron is a flat two-step search
regardless of mode. Drop the tail from the base hints and append a
"~N Comic Vine requests/comic" suffix only when Comic Vine is an active
source, so a Metron-only run no longer shows a count that doesn't apply.
The number derives from the existing COMICVINE_CALLS_BY_MODE constant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(fs): don't let one unreadable folder crash the library scan
The poller's DiskSnapshot._walk() called os.scandir() with no guard
around the directory open, so a single permission-denied folder (e.g.
a Synology /comics/#recycle bin) raised PermissionError that propagated
up and killed the LibraryPollerThread, aborting the scan of every other
folder (issue #795).
- Wrap os.scandir so an unreadable/vanished directory is logged and
skipped instead of aborting the whole poll, and widen the per-entry
guard to cover entry.is_dir(), which can also raise PermissionError.
This matches the os.walk default-onerror behavior the watcher relies
on.
- Register the OS/NAS metadata basenames the filters module already
documented but never populated (@eaDir, #recycle, __MACOSX,
Thumbs.db, desktop.ini), so the walker skips the recycle bin entirely
and NAS/OS junk never enters the library.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* update version to 2.1.2
* fix typechecking
* news for v2.1.2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* format
* Squashed commit of the following:
commit 9db6fe273622635700defb5c9015bf540630e40a
Merge: c8984b9ed ed38cb555
Author: AJ Slater <aj@slater.net>
Date: Sat Jul 4 15:57:53 2026 -0700
Merge branch 'develop' into online-estimate-consume-comicbox
commit c8984b9ede16f82f398501adb49c58d5428c168d
Merge: 2b2e63a35 cd84ed99e
Author: AJ Slater <aj@slater.net>
Date: Sat Jul 4 13:22:52 2026 -0700
Merge branch 'develop' into online-estimate-consume-comicbox
commit 2b2e63a35013de0cd5593c8c5cadb360ffbd23ab
Author: AJ Slater <aj@slater.net>
Date: Fri Jul 3 20:36:09 2026 -0700
feat(onlinetag): consume comicbox 4.1.0 estimate; drop the codex copy
Pin comicbox ~=4.1.0 and move the online-tag run estimate onto its
comicbox.online_estimate.estimate_run() home:
- estimate.py becomes a thin seam over comicbox: estimate_seconds()
forwards to estimate_run().seconds and re-exports SOURCE_RATE_PER_MINUTE.
The request/rate constants and math are deleted -- comicbox owns and
tests them now.
- The launcher dialog's per-source rates and per-comic request model derive
from comicbox via a new tagging-estimate.json (choices/onlinetag.py,
build-choices); only display labels stay in the component, so the JS
estimate can no longer drift from the backend.
- The codex estimate test slims to an adapter / re-export guard.
Prep branch: the ~=4.1.0 pin does not resolve until comicbox 4.1.0 is
published, so uv.lock is untouched and CI targets that shell out to `uv`
will fail until then. Post-publish, run `uv lock`; the change was validated
locally with the 4.1.0 modules installed into the venv.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* refactor(onlinetag): extract resume-param sanitizer to cut complexity
AdminOnlineTagResumeView.post crossed radon's C threshold once the resume
descriptor sanitization landed. Move that logic (sources tuple coercion +
dropping keys no task field accepts) into a module-level helper; the view
falls to rank B and reads more directly. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(onlinetag): reference defined constant in match-mode hint
matchModeHint read an undefined COMICVINE_CALLS_BY_MODE, throwing a
ReferenceError on every launcher-dialog render (and failing
tests/unit/launcher-dialog.test.js). Point at the real
TAGGING_ESTIMATE.comicvineRequestsByMode map that callsForSource
already uses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(settings): nest comicbox loglevel/delete_keys under general section
An un-nested overlay is silently ignored by confuse.
* fix(onlinetag): resolve prompts against current DB path, surface apply failures
Answering a deferred prompt fetched the chosen issue against the path
serialized into the prompt at scan time. When an earlier write for the
same comic ran with rename enabled (the comic's other source's prompt,
or a stored-id prefetch), that path was stale and the apply died with an
uncaught FileNotFoundError — after the prompt was already consumed, so
the admin's pick vanished with no feedback.
- _apply_resolution now re-reads the comic's path from the DB by pk;
a missing row reports to the Tagging error panel instead of fetching
a dead path.
- fetch/replay failures (ComicboxError, OSError) and non-resolving
explicit ids now land on the Tagging error panel instead of only the
log, since the pick can no longer be re-prompted.
- stored-id prefetch and tag_by_id also catch OSError so a vanished
file degrades gracefully.
- regression test for the COMICBOX_CONFIG general-section overlay
(un-nested loglevel/delete_keys were silently ignored, letting
comicfn2dict remainders like "(0000)" leak into rename targets).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps and format
* Native OIDC single sign-on (Admin Auth tab) (#798)
* feat(auth): native OIDC login via django-allauth
Codex becomes an OIDC Relying Party (Authentik/Authelia) with a
config-gated login flow:
- [auth.oidc] TOML section + CODEX_AUTH_OIDC_* env overrides
- allauth apps installed unconditionally; behavior gated on
AUTH_OIDC_ENABLED (all OIDC paths 404 when off)
- CodexSocialAccountAdapter: username linking (superusers included,
documented trust boundary), optional email linking, claim-chain
username mapping with sub-hash collision suffix, groups-claim sync
to existing Django groups, admin-group grant/revoke, error
redirects to the SPA (never an allauth template)
- Branded throttled init endpoint /api/v4/auth/oidc/login; allauth
login/callback mounted at /sso/ (outside the namespaced API tree so
allauth's internal reverses work)
- RP-initiated logout URL via cached discovery document using the
spec's client_id parameter (no stored tokens needed)
- /session payload gains public oidcEnabled/oidcProviderName/
oidcLoginUrl and authenticated oidcLogoutUrl
- Profile username locks per-user when an OIDC identity is linked
- OIDC failures reuse the failed-login log line format for fail2ban
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): SSO login button, RP logout, and sso-error page
- auth store: oidc admin flags, loginSSO() full-page navigation,
logout() follows oidcLogoutUrl for RP-initiated logout
- SsoLoginButton shared by the login dialog (with divider) and the
unauthorized lock screen
- /auth/sso-error route + page mapping backend error codes to human
messages, with retry hidden for non-retryable codes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(auth): OIDC setup guide + complete tinyauth forward-auth recipe
- README: native OIDC section (config table, redirect URI with prefix,
Authentik/Authelia walkthroughs, identity-mapping and admin-linking
trust warning, session-lifetime and OPDS caveats)
- README: full nginx auth_request recipe for tinyauth with header
override hardening, Traefik/Caddy equivalents, and a forward-auth
deployment checklist (OPDS + WebSocket gating, spoof test)
- schema test: allauth views stay out of the OpenAPI schema
- test typing fixes surfaced by basedpyright
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(settings): nest comicbox loglevel/delete_keys under general section
An un-nested overlay is silently ignored by confuse.
* fix(onlinetag): resolve prompts against current DB path, surface apply failures
Answering a deferred prompt fetched the chosen issue against the path
serialized into the prompt at scan time. When an earlier write for the
same comic ran with rename enabled (the comic's other source's prompt,
or a stored-id prefetch), that path was stale and the apply died with an
uncaught FileNotFoundError — after the prompt was already consumed, so
the admin's pick vanished with no feedback.
- _apply_resolution now re-reads the comic's path from the DB by pk;
a missing row reports to the Tagging error panel instead of fetching
a dead path.
- fetch/replay failures (ComicboxError, OSError) and non-resolving
explicit ids now land on the Tagging error panel instead of only the
log, since the pick can no longer be re-prompted.
- stored-id prefetch and tag_by_id also catch OSError so a vanished
file degrades gracefully.
- regression test for the COMICBOX_CONFIG general-section overlay
(un-nested loglevel/delete_keys were silently ignored, letting
comicfn2dict remainders like "(0000)" leak into rename targets).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(auth): move OIDC config from codex.toml to the Admin UI Auth tab
OIDCSettings DB singleton (EmailSettings pattern) becomes the sole
config source, read at request time:
- OIDCSettings model + migration 0047 (seeds pk=1, one-time courtesy
import of any pre-GUI [auth.oidc] TOML values); client_secret
encrypted at rest via EncryptedCharField
- get_oidc_settings()/oidc_enabled() in settings.db; cachalot makes
admin edits live on the next request, no restart
- codex/oidc.py rewired to request-time reads; new adapter
list_apps override builds an unsaved SocialApp from the row
(per-app settings['scope'] wins in allauth's get_scope), so
disabled state keeps allauth's own DoesNotExist -> 404 gating
- RP-initiated logout and session flags read the row
- AdminOIDCSettingsView GET/PUT (write-only secret + clientSecretSet
mirror, discovery-cache invalidation on save) and AdminOIDCTestView
(discovery-document probe) at /api/v4/admin/oidc-settings[/test]
- New Admin UI Auth tab mirroring the Email tab: draft/dirty
tracking, never-echoed secret with Clear Credential, redirect-URI
display, Test Connection endpoint report
- [auth.oidc] TOML section and CODEX_AUTH_OIDC_* env overrides
removed; README updated
- Tests now seed the DB row instead of patching module constants
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* format
* update deps and fix
* fix(admin): gate the OIDC enable switch on server URL + client ID
The Auth tab's Enable OIDC Login checkbox is disabled until a valid
server URL and a client ID are entered (it can always be unchecked so
clearing a field never strands the switch). The serializer enforces
the same invariant for API clients and partial updates that blank a
prerequisite while enabled — previously such a save produced a
silently inert enabled=true row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(admin): Auth tab gains Account & Access flags, tinyauth note, name gate
- Move the Account & Access flag cards (Registration, Verify New User
Email, Non-Users) from the Users tab to the Auth tab — they govern
how people get in, which is that tab's subject
- Auth tab prose explains that forward-auth gateways like tinyauth are
not OIDC providers and points them at Remote-User header auth, which
coexists with OIDC
- Provider name joins server URL and client ID as an enable
prerequisite, in the UI switch gate and the serializer invariant
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(admin): visually nest the OIDC subsections under their header
AdminSection gains a sub variant: a small uppercase overline title (h4,
$text-meta) and an indented left rule, with tighter sibling rhythm than
top-level sections. The Auth tab wraps the whole OIDC block — prose,
Identity Provider, User Mapping, Logout, and Test Connection — in one
parent 'OIDC Single Sign-On' AdminSection with the config groups as sub
sections, so their subordination to the OIDC header is unmistakable
next to the sibling Account & Access section. Documented in DESIGN.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(admin): collapse the OIDC section when OIDC is disabled
Most admins never configure OIDC, so the section body — prose, config
sub-sections, and Test Connection — hides behind an AdminExpandToggle
disclosure. It starts expanded only when OIDC is already enabled;
otherwise a one-line hint summarizes what's inside next to a Configure
toggle. The disclosure is initialized once from the saved state so
saving a disable doesn't slam the panel shut mid-edit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(admin): plain-English hints for PKCE and other OIDC jargon fields
PKCE, Client ID, Username Claim, and Groups Claim now carry hints an
admin who has never touched OIDC can act on — including what a claim
is and why PKCE should stay on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): authentik + tinyauth manual test harness in test-proxy/
Adds a docker-compose IdP stack and nginx wiring so SSO can be manually
verified before release:
- compose.yaml: authentik (OIDC provider, :9010) + tinyauth (forward
auth, :3232), everything bound to localhost with throwaway creds
- authentik/blueprints/codex-test.yaml: auto-applied fixtures — readers
and codex-admins groups, testuser/testadmin, and the codex-test OIDC
client with callback URIs for proxied and direct, prefixed and bare
- forwardauth.conf: nginx :8081 gating Codex behind tinyauth
auth_request with an overriding Remote-User header
- README.md: step-by-step test matrix covering native OIDC (login,
group sync, admin mapping, RP logout, linking, error page, disabled
404) and forward-auth (login, gating, spoof-proofing, coexistence)
tinyauth DB path pinned to the writable /data volume (workdir is
root-owned). test-proxy/ excluded from eslint: authentik !Find tags and
compose healthcheck arrays require flow-style YAML the yml plugin bans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): run the harness nginx as a compose service
nginx joins authentik + tinyauth in compose.yaml, so only Codex runs on
the host. The native bin/run-test-proxy.sh path still works — both share
server.conf/forwardauth.conf, with the sole native-vs-container
difference (backend addresses) isolated into named upstreams:
- upstreams-native.conf: localhost backends (host nginx)
- upstreams-docker.conf: host.docker.internal + tinyauth service name
- connection-upgrade.conf: the ws-upgrade map, now shared
- ssl-listen.conf / ssl-listen-none.conf: SSL/QUIC listeners split out so
the container serves plain HTTP (native keeps the 8443 listeners)
The compose nginx mounts these into the stock image's conf.d and reaches
host Codex via host.docker.internal (extra_hosts host-gateway for Linux).
Also fixes a latent harness bug that would break OIDC through the proxy:
X-Forwarded-Host used $host (strips the port), so Django's
build_absolute_uri produced a portless redirect_uri that couldn't match
the registered callback. Now $http_host, port included.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): fix tinyauth boot crash on localhost app URL
tinyauth v5 derives a cookie domain from its app URL at startup and
rejects single-label hosts and IPs ('invalid app url, must be at least
second level domain'), so http://localhost:3232 crash-looped. Move the
forward-auth path onto *.localtest.me (all subdomains resolve to
127.0.0.1 via public DNS, every browser, no /etc/hosts):
- tinyauth app url -> http://tinyauth.localtest.me:3232
- gated Codex -> http://codex.localtest.me:8081
- shared cookie -> .localtest.me (spans both)
The @tinyauth_login redirect and README Test 2 follow. OIDC/authentik
stay on localhost (no cross-host cookie needed there).
Also documents in README Troubleshooting that the harness publishes only
9010/8080/8081/3232 and never binds Vite's 5173 — a blocked HMR is a
stale vite process, and 8080/8081 clashes come from running native
make dev-reverse-proxy alongside the compose nginx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): set authentik provider grant_types; drop IPv6 host-gateway
Two issues from the first live OIDC run:
- 'Login with Authentik' failed with authentik logging 'Invalid
grant_type for provider'. authentik 2026.x added an explicit
grant_types model field that defaults to an EMPTY list, so a blueprint
that omits it creates a provider allowing no grants and the authorize
step returns invalid_request. Set grant_types: [authorization_code,
refresh_token] on the provider.
- nginx logged 'connect() to [fd..::254]:9810 Network unreachable' then
fell back to IPv4. The IPv6 came from extra_hosts host-gateway (a
Docker Desktop IPv6 ULA gateway Granian doesn't listen on). Comment it
out — Docker Desktop provides an IPv4 host.docker.internal built-in;
Linux users uncomment it.
README troubleshooting covers both, including re-applying the blueprint
to an already-running authentik.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auth): refresh public flags when OIDC is toggled or on logout
The 'Login with <provider>' button (adminFlags.oidcEnabled) went stale
after disabling OIDC: OIDCSettings is a singleton with no
admin.flags.changed websocket broadcast, and logout() left adminFlags
untouched, so the button lingered on the login screen until a manual
page reload.
- admin.updateOidcSettings now calls auth.loadAdminFlags after a save,
resyncing the public OIDC flags immediately.
- auth.logout now re-fetches public flags (except when doing an
RP-initiated full-page redirect, which reloads anyway), so the
logged-out login screen always reflects current settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* chore(lint): clear radon CC/MI and remark warnings
- codex/oidc.py: extract CodexSocialAccountAdapter._sync_admin from
_sync_user (rank C -> B); keyword-only bool arg for FBT001.
- tests: split the 940-line test_onlinetag_session_manager (MI rank B,
pre-existing on develop) — move the TagPassRunner and stored-id-map
classes into test_onlinetag_tag_pass.py, importing the shared doubles
from the session-manager module (as test_opds_schema already does).
Both files now MI rank A.
- test-proxy/README.md: wrap the bare http://localhost autolink in <>
and the [fd..::254] nginx error in backticks so remark-lint stops
reading it as a link reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* bump version 2.2.0
* bump news
* update deps and comicbox
* adapt ComicVine credential check to simyan v3 (comicbox 4.1.1)
simyan 3.0 removed the cache= constructor kwarg. The credential check
now passes cache_expiry=DO_NOT_CACHE with the cache/ratelimit sqlite
files in a throwaway temp dir, so validation always hits the network
(api_key is excluded from simyan's cache key) and leaves no files
behind. Also note comicbox 4.1.1's ComicVine improvements in the
v2.2.0 news.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* test(onlinetag): assert merge flag is forwarded, not comicbox's arithmetic
test_estimate_seconds_passes_merge_flag pinned a hardcoded 1000.0 that
went stale when comicbox 4.1.1 changed Comic Vine pacing to bill the
busiest resource pool (simyan 3.x per-endpoint buckets) instead of the
request total. The codex seam only forwards to comicbox.estimate_run, so
re-derive nothing here: assert the flag's defining effect — merge sums
every source's pace, so it costs strictly more than first-match-wins —
which proves forwarding without coupling to comicbox's rate model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
comicbox 4.3.0 (mokkari 4 header-driven Metron rate limits), pinia 4.0.2,
vue-router 5.2, vuetify 4.1.5, vite 8.1.5, eslint plugin updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v2.2.1: show Metron account rate limits live (comicbox 4.3.0 / mokkari 4)
Testing Metron credentials in the Admin panel now reports the account's
real burst and daily limits read off the validation response's
X-RateLimit-* headers — the daily limit reflects the user's Metron donor
tier. The online tagging status table shows the live daily budget as a
run progresses, via comicbox 4.3.0's newly wired
OnlineSession.rate_limit_status().
Also removes a ty ignore in tests/opds_schema.py made stale by the dep
updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* v2.2.1: community ratings replace critical rating (comicbox 4.4.0)
comicbox 4.4.0 remaps ComicInfo CommunityRating / CBI rating — the tags
that fed critical_rating — to its new community_rating field
(average_rating + rating_count, Metron-filled), and critical_rating no
longer persists to any format. Migration 0048 renames the column
(values carry: same tags, same scale), adds community_rating_count and
alternative_issue number/suffix columns, and remaps user settings that
reference the old key (order_by, table_columns JSON) in RunPython.
Community rating gets full browser parity: sort (Avg aggregate), table
column, sidebar filter, and field search incl. rating_count. The
metadata dialog shows '4.2 / 5 (128 ratings)' and the tag editor edits
the pair, with the count enabled for MetronInfo only (the only format
that persists it). Alternative issues import and display ('#43.5AU').
Sidecar backups tolerate the rename both ways: schema column renamed,
restore remaps legacy critical_rating filter columns, order_by values,
and table_columns keys from old dumps.
Also: ty ignores for dep-bump invalid-method-override errors in
vuetify serializer fields; test fixtures rebuilt with deterministic
past-dated zip mtimes (the importer prefilter skips future-dated
embedded mtimes as unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* v2.2.2: fix rotated pdf page serving (comicbox-pdffile 0.6.3)
PDF pages scanned upside down or sideways and righted by the pdf's
rotation attribute displayed rotated when the reader served them as
images; pdffile 0.6.3 re-renders rotated image-dominant pages instead
of serving the stored bytes. Also stops a read-only page serve from
rewriting a pdf on disk when MuPDF repairs its content streams in
memory (close() no longer saves on the repair-dirty flag).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps, including comicbox 4.5.0
* Fix silent no-op when clearing tag editor fields
Clearing a field encoded the clear as an empty patch value (""/{}/null),
but comicbox prunes empty values on schema load and a merge write can
only add or replace — so every "clear field" action was a silent no-op
on the archive.
Cleared and emptied fields now travel as comicbox delete_keys glom paths
(new in comicbox 4.5.0): buildPatch returns {patch, deleteKeys} ->
tag-write and preflight POSTs -> serializer -> BulkTagWriteTask ->
BulkWriteItem. A clear-only edit sends an empty patch and still writes.
Rename previews layer the delete keys onto the preview config so a
cleared series or issue drops out of the previewed filename too.
Also document why the read-side COMICBOX_CONFIG must never become the
write base config: comicbox unions a write's delete_keys with the base
config's, and the read config skips every schema field codex doesn't
consume, so using it would strip all of them from the user's archive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix lint errors from the ruff 0.16 upgrade
The dependency update to ruff 0.16.0 enabled rules that flag four
pre-existing spots, failing lint (and CI) independently of any code
change: RUF036 None-last in two exception handler unions, PLC0206 dict
iteration without .items(), and PLR0917 too many positional arguments.
The mail backend's positional signature mirrors Django's
SMTPBackend.__init__ to stay a drop-in, so PLR0917 joins the PLR0913
suppression already there rather than changing the signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update comicbox
* Clear monochrome by deleting the tag, not writing false
comicbox 4.5.1 fixes ComicInfo's BlackAndWhite in both directions, so
monochrome is now a real tri-state tag: Yes, No, or absent. The clear
icon set the patch value to false, which now writes <BlackAndWhite>No
</BlackAndWhite> — asserting the comic is known to be color instead of
removing the tag. The bug was invisible before 4.5.1 because nothing
was written at all.
Cleared monochrome joins the other cleared fields in delete_keys;
explicitly unchecking the box still patches a false.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update devenv and deps
* update claude rules about telemetry
* Fix anonymous stats sending and report what codex grew into
v2.2.3. Two problems: stats had not been sent by any install since v2.0.0,
and what they would have sent no longer described codex.
Transport. Replacing requests with urllib left the server credentials in the
URL. urllib hands the whole netloc to http.client, which reads the password
as a port and raises InvalidURL, so every send failed before opening a
socket. A leftover requests-style raise_for_status() would have broken a
successful send too. Both were logged at debug, which is why this went
unnoticed for months; failures now log at warning. Credentials move to an
Authorization header, and CODEX_TELEMETER_URL points a dev install at a local
chronicle.
Payload, now wire version 2. Adds counts and settings for everything since
v1.12: read-only libraries, custom covers, favorites, bookmarks, community
ratings, browser table view, online tagging, single sign on, email, rate
limits and reverse-proxy deployment. Identifiers are counted by source and
type, which is the only durable measure of online tagging use.
It stays counts, booleans and closed-enum values, per the telemetry privacy
rule. Nothing an administrator typed is included: no paths, account or group
names, search terms, banner text, service URLs, API keys or credentials. A
setting holding text or a secret reports only whether it is set. Identifier
source names come from comic files, so they are mapped through comicbox's
known sources and anything else becomes "other" before it leaves the process.
tests/test_telemeter_privacy.py seeds a sentinel into every such field and
fails if one reaches the payload, then requires every value to be a number, a
boolean, or a string from a closed vocabulary.
The Admin Stats tab renders every new section, so the page still shows the
whole report. Two label fixes there: the metadata table pluralizes by
appending "s", which produced "Storys" and "Comic Metadata Importeds".
Also: the identifiers section never reached the admin endpoint.
AdminStatsRequestSerializer fills in every section it declares, so params is
always truthy and the section gate drops anything undeclared - an empty table
with no error. Declared it, and added a test so the next section added to
StatsSerializer cannot vanish the same way. FILE_TYPES_CHOICES was missing
CB7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* Support comicbox 4.6.0 series alternative names
comicbox 4.6.0 records a series' localized and variant titles as
reprints: from MetronInfo AlternativeNames and Reprints tags, and now
from Metron and Comic Vine online tagging. Codex dropped the field
entirely -- reprints sat outside USED_COMICBOX_FIELDS, so the read
config told comicbox to skip parsing it and the aggregator popped it as
a backstop.
Import them into a new denormalized Reprint model keyed on series name,
volume number, issue and language. Series and Volume are browse
collections, so reusing them would have hung phantom rows off the
browser for every alternate title. A reprint may carry only a series
sort_name, which stands in when the name is absent; one with neither is
dropped.
Alternate names reach the metadata panel, the tag editor, a sortable
browser column and filter, and a distinct alternate_series FTS column,
so a comic filed under a localized title is findable by it. The column
is distinct rather than folded into series to keep the series: token
exact; unqualified searches match either.
Online matching now recognizes Comic Vine volume aliases, which codex
inherits by delegating matching to comicbox. The match prompt shows
each candidate's alternative names, since a comic filed under a
localized title matching its canonical volume otherwise reads as a
wrong match, and passes the chosen candidate's volume id, which was
plumbed end to end but always null.
Drop the auto_threshold setting. It was parsed, serialized and
round-tripped through resume, but never reached OnlineSession, which
accepts no threshold.
Reconcile sidecar columns on open. schema.sql is all CREATE TABLE IF
NOT EXISTS, so a sidecar predating a release never gained its new
settings_filters columns and the next dump failed with "no such
column". The wanted columns derive from the schema file rather than a
list that would rot the same way. This also repairs the column the
community_rating rename added.
Already-imported comics pick alternate names up on a Force Update Tags;
adding a field to the whitelist does not invalidate metadata_mtime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps and devenv
* bump news for comicbox 4.6.1
* update devenv
* fix cron double enqueue of telemeter task
* test crond double enqueu fix
* test telmemeter logging fixes
* feat(api): reintroduce Swagger UI at /api/v4/
The interactive docs were lost in the v4 cutover (9b8314d3b, v2.0.0)
when codex/urls/api/v3.py was deleted; only the raw schema route
survived. Mount SpectacularSwaggerSplitView at the v4 root, gated on
FEATURES.swagger — the flag that until now only switched a CSP overlay
for routes that no longer existed.
The split view keeps its init javascript in a second same-origin
request, so no inline <script> needs a nonce.
Also fix the CSP overlay it depends on: it listed the jsdelivr bundles
under script-src only, but the pdfs-dist overlay declares
script-src-elem, which masks the script-src fallback for element loads
and would have blocked both bundles. List them under both directives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* v2.2.4
* sub api v3 for v4
* feat(tagging): authenticate to Metron with an API key
Metron issues API tokens now, so the admin Tagging tab offers a single
API Key field and no longer mentions usernames or passwords. The key
lands in a new encrypted metron_key column (migration 0050) and reaches
comicbox as OnlineCredentials.metron_key, which mokkari uses as its
Bearer token. Preferring the key over a login needs no code: mokkari
drops basic auth whenever a token is present.
Logins saved before this release keep working. Every path that decides
whether Metron is configured -- the scan session, tag-by-id, credential
testing, and the telemetry boolean -- accepts a key or a username and
password pair, matching comicbox's own is_configured. The validator
passes api_token=None rather than "" so an absent key doesn't send an
empty Bearer header and defeat that fallback.
Writing the key retires the login it replaces: a PUT carrying
metron_key blanks metron_user and metron_password, so saving a key or
clearing credentials both leave no stale login behind. A save that
omits the field (a custom-URL edit, the settings auto-save) leaves a
stored login alone.
The user-data sidecar exports metron_key too, and restore skips
coalesced columns a backup predates -- sqlite3.Row raises on a missing
column, so an older sidecar would otherwise crash the restore.
Requires comicbox 4.7.1, which also warns once per process when basic
auth is what actually gets used.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* feat(tagging): remove the custom URL fields for Metron & Comic Vine
The Metron custom URL never did anything: mokkari hardcodes
METRON_URL = "https://metron.cloud/api/{}/" and exposes only dev_mode as
an alternative, so comicbox's metron source drops the credential url and
warns that it is a no-op. The Comic Vine override did work via simyan's
base_url, but a comic server has no use for pointing at a different
Comic Vine.
Removes both fields from the model (migration 0051), the admin and
validate serializers, the validate view's credential tuple, all three
librarian consumers, the two telemetry booleans and their stats
serializer fields, and the user_data backup/restore path. comicbox keeps
its OnlineCredentials url fields; codex just stops passing them, so
test_online_credentials_fields_stable still expects them.
Old sidecar backups carrying the dropped columns restore fine: the
restore comprehension walks its own allowlist rather than the row, the
same way it already ignores the retired active_session_id.
Also fixes a bug from 8fbf7e11f in the same DDL: schema.sql never gained
a metron_key column, so the tagging_defaults upsert failed with "no such
column" — and since _dump_queryset only logs per-row failures, every
backup silently wrote zero tagging rows. Adds the column (_reconcile_
columns retrofits existing sidecars) and a dump round-trip test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): clear the two outstanding ty errors
test_snapshot_diff: a real annotation bug. The ``_snapshot`` helper took
``models: dict[str, type]`` but assigns it straight to
``Snapshot._path_to_model``, which is ``dict[str, type[Model]]``. All
five call sites already pass Comic or Folder, so the parameter was just
looser than both its callers and its destination.
test_bookmark_filter_isolation: unavoidable suppression. The mixin
declares ``self.request: Request`` and the helper deliberately assigns a
SimpleNamespace, since its whole point is to skip DRF's request
lifecycle. A ``cast`` traded the ty error for basedpyright's
reportInvalidCast (the types don't overlap), and its suggested
double-cast through ``object`` reads worse without making the stub any
more of a Request. Adds ``# ty: ignore[invalid-assignment]`` beside the
existing pyright ignore, per the two-checker convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tagging): present Metron as an API key source & warn on legacy logins
The admin Tagging tab called Metron's credential a "credential" while Comic
Vine's twin panel called it an API key. Metron authenticates with a
metron.cloud API key now, so say so — except on an install whose only stored
credential is still the legacy username & password.
- Metron's status, clear button, and confirm dialog switch between an API-key
and a legacy-credential label set, keyed on metronKeySet.
- A legacy-only install gets a warning-colored deprecation notice linking to
Metron's token authentication announcement, and the panel opens itself so
the notice isn't buried behind a click.
- The Online Tagging dialog carries a one-line version of the same warning,
shown only when a legacy login is stored AND this session would query
Metron (selected on the Search tab, or the id's source on the By ID tab).
- Comic Vine's save button and the source-disabled tooltip say API key too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update devenv
* update deps
* fix(importer): import a renamed comic's new tags instead of failing
Tagging a comic with rename enabled produced a failed import for the
pre-rename path, and the tags that had just been written never imported
at all.
The tag write and the rename land in one watch batch: a modify naming
the old path, plus a delete+add that inode matching pairs into a move.
Move detection never looked at the modify, and the task builder pruned
modified paths only against move destinations, so the task carried
files_moved={old: new} alongside files_modified={old}. The importer
applies moves before reading, so the read opened a path that no longer
existed.
Remap modified paths through the move map rather than dropping
destinations. Sources become their destination (the write-then-rename
every external tagger performs, codex's own included), and destinations
survive, which the poller emits deliberately for a move whose stats
also changed.
Stop the move phase from refreshing Comic.stat. The stored stat means
"the file as of its last tag import", so refreshing it on a move erased
the only evidence the read phase had that the renamed file's contents
had changed too -- the tags were lost rather than deferred. A pure
rename leaves inode, mtime and size alone, so the preserved stat still
matches disk.
Also stop recording failed imports for files that vanished mid-import:
the row was queued before presave() stat'd the path, so the OSError
meant to drop it did not. Key the failed-import map by str so its
membership test against db paths can match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v2.2.5
* feat(tagging): combine online search and tag-by-id into one pass
The Tag Online dialog's Search and By ID tabs become one pane. Ids are
pinned per source: a pinned source is fetched by that issue id while the
unpinned ones search, in a single comicbox lookup, so merge_all_sources
merges across both. The submit button reads Search, Tag by ID & Search,
or Tag by ID accordingly, and entering an id selects its source.
Ids now ride on tag-sessions/start as {source: token}. That retires the
parallel POST /admin/tag-by-id path entirely -- AdminTagByIdView,
OnlineTagByIdTask, TagByIdRequestSerializer -- so tagging by id gains
session status, resume, and the write pipeline the scan already had.
Also drops dry_run, which the start view accepted and never read.
run_session skips the DB stored-id prepass when ids are pinned: the
prepass pops the comic out of comic_paths, which would leave the
unpinned sources nothing to search.
Needs comicbox 4.8.0 for OnlineSession(ids=...). That release is not on
PyPI yet, so the pin here is still ~=4.7.1 -- bump it after publishing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* style(news): prettier wrap the online tagging entry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tagging): restore the custom URL for Comic Vine
v2.2.4 (0dd3e5199) removed the custom URL fields for both online
sources. Metron's was correctly retired — mokkari hardcodes
METRON_URL and comicbox warns the url is a no-op — but Comic Vine's
worked: comicbox passes it through OnlineCredentials.comicvine_url to
OnlineSourceCredentials.url and on to simyan's base_url, which is what
lets tagging run against a Comic Vine proxy or mirror.
Restores the Comic Vine half only, no comicbox change needed:
- Model field + migration 0052. A plain URLField rather than an
EncryptedCharField, because unlike the API keys it is not a secret
and must read back for the admin form's placeholder.
- Admin serializer (read+write), validate request serializer, and the
validate view's _CREDENTIAL_FIELDS.
- All three librarian consumers: the scan session's OnlineCredentials,
the explicit-id auth mapping, and the credential validator's simyan
base_url, so Test checks the endpoint the scan will actually use.
- Backup/restore sidecar column, serializer, and restore allowlist.
_reconcile_columns retrofits existing sidecar files; a legacy
metron_url column in an older backup stays silently ignored.
- Telemetry reports only bool(comicvine_url) — never the value.
- The admin Tagging tab's Comic Vine panel gets the field back, its
save button reverts to "Save Comic Vine Credentials" now that a
URL-only save is possible again, and Clear removes both.
A URL alone is not a credential: it must not satisfy the session
manager's configured-sources gate, enable the source checkbox, or pass
validation without a key. Tests pin all three.
URLs dropped by 0051 are unrecoverable; admins re-enter them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* bump version and update deps
* bump dockerfile source version
* granian 2.8.0
* update deps
* update devenv
* update devenv and deps
* use nodejs26 in the builder
* update deps
* remove old ty ignore
* update deps
* fix radon complexity warnings
Three items `make complexity` flagged, all pure refactors.
AdminOnlineTagStartView.post (CC 11 -> C): three near-identical
"request value or stored default" blocks collapse into a
_FLAG_DEFAULT_FIELDS mapping plus a _resolve_flags helper.
test_telemeter_privacy._vocabularies (CC 12 -> C): split by vocabulary
source into _choice_vocabularies, _identifier_bucket_vocabulary and
_tagging_vocabularies, with the literal and OIDC sets hoisted to module
constants. The same string set is checked; the walk is not widened.
test_onlinetag_session_manager (MI 17.37 -> B): 916 lines in one module,
every function already rank A. Split along its seams into the scan pass,
prompt resolution and credentials, with the doubles and the comic factory
moved to tests/onlinetag_session_fakes.py. All 31 tests come across
unchanged; the move dedupes the BulkTagWriteTask filter into
write_tasks() and the prompt dict into a _prompt() factory.
test_onlinetag_tag_pass now imports the fakes from their new home.
make complexity, lint, ty clean; 815 pytest + 371 vitest pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update deps
* fix(importer): skip comic moves onto claimed paths
A move whose destination path already belonged to another comic row
violated the (library, path) unique constraint inside bulk_update. The
IntegrityError aborted the whole import before the delete phase could
clear the stale row, so the poller rebuilt the same task and crashed on
every subsequent scan.
Mirror the folder guard for comics and custom covers: drop moves onto
paths an existing row holds, and moves that two sources in one batch
claim, which files_moved can express because it is a plain dict rather
than a bidict. Skipping converges. The destination row's stat refreshes,
the next scan sees two rows sharing an inode and suppresses the bogus
move, and the stale source falls through to the delete phase.
Also contain any move phase failure in all three move steps so a bad
batch degrades to a skipped phase the next scan reconciles instead of
aborting the import. This covers the same class of crash in the folder
step, where converting dirs_moved to a bidict raises on the duplicate
destinations the watcher can emit.
Fixes #807
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* bump version
* update deps
* update deps
* fix(update): make codex self-update work on macos and linux
The self-update was not flaky, it was inert.
Nothing ever asked for an automatic update. update_latest_version()
had an `update` hook that queues JanitorCodexUpdateTask and no caller
passed it, and the update task was not in the nightly fan-out, so the
Auto Update flag promised a daily upgrade that could never happen. The
nightly check now forces a fetch and chains into the update when the
flag is on. The flag gate moves to that scheduling point, so the admin
Jobs tab button updates whether or not the flag is set -- it used to
silently no-op, since the flag defaults off and the button sends
force=False.
The installer ran `sys.executable -m pip install --upgrade codex` with
no timeout, no captured output, and a blanket except that swallowed
every failure while still logging "updated to the same version". uv and
pipx environments have no pip and the docker image uninstalls it, so
those installs always failed invisibly. Now: pip if importable, else
`uv pip install --python <sys.executable>`, else an ERROR naming both
and pointing docker at a new image. Failures log the installer's own
stderr, a hung installer times out instead of wedging the scribe queue
behind it, and the restart only fires when the install really happened.
Restarting exec'd __file__, which relied on run.py's executable bit and
its `#!/usr/bin/env python3` shebang resolving to a python that has
codex installed. Under macOS, pipx, uv, systemd and launchd that is
frequently a different interpreter and the server never came back. Exec
`sys.executable -m codex.run` instead.
_is_outdated() called Version() on unvalidated strings, raising on a
fresh install's empty cache and on a source checkout's "test" version.
Version comparison is now codex.version.is_outdated(), total by
construction, shared by the janitor and the version view.
The browser could not announce anything: semverGreaterThan(a > b) passed
one boolean into a two-argument function so `outdated` was always false,
and the comparison read 1.0.9 as newer than 1.1.0. The server ships
`outdated` and `docker` in the version payload and the javascript
comparison is gone. The footer renders a codex orange "upgrade to codex
vX.Y.Z" link to the Update Codex job, or to the image repo in docker,
where codex cannot install over itself. The Jobs tab grows section
anchors, names the version the job would install, and explains the
docker case.
While the cache is empty every /api/v4/version request queued a fetch
task, each on its own thread with a 5 second PyPI call. Add a process
wide in-flight lock and a cooldown after a failed fetch.
Tests cover the version comparison, the payload, installer selection and
failure paths, the flag gate and fetch guards, the nightly wiring, and
both frontend surfaces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(proxy): document proxy_pass_header Server for nginx
nginx hides the upstream Server header by default (along with Date,
X-Pad and X-Accel-*) and substitutes its own, so codex/<version> never
reaches clients behind a reverse proxy. Granian passes the header set by
CodexMiddleware through untouched; nginx is the only clobberer.
Add proxy_pass_header Server to the README's example location, plus a
subsection covering the curl verification, why server_tokens off is not
a substitute, and the array-directive inheritance trap (a location that
declares any proxy_pass_header drops the one inherited from server{}).
Set the same directive in the test-proxy harness so the documented
config is the one actually exercised.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update new for docs too
* update deps
* update deps
* fix(opds): vary the response cache on User-Agent (#813)
OPDS responses were cached without varying on User-Agent while the body
depends on it, so a client could be served a variant rendered for a
different one until the entry expired. UserAgentNames switches facet
emission (FACET_SUPPORT), download mime types (SIMPLE_DOWNLOAD_MIME_TYPES),
order facet suppression (CLIENT_REORDERS) and absolute hrefs
(REQUIRE_ABSOLUTE_URL), and cache_page keys only on the URL plus the
headers named in Vary.
Add User-Agent to the existing vary_on_headers in opds_cached, which
covers every wrapped v1 and v2 feed, start, manifest and opensearch
route. Vary already includes Cookie, so per-session keys existed anyway
and this barely fragments the cache further; it also corrects what
intermediary caches are told. Cover routes keep the narrower vary since
covers don't depend on the client, and the static authentication
document is left alone.
Adds tests/test_opds_cache.py asserting the Vary header names User-Agent
and that a feed primed by one client isn't replayed to another. Both
fail without the fix.
Fixes #811
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): send v1 entry contributors, which were silently dropped (#814)
OPDS1TemplateEntrySerializer declared the field as `credits`, but the
entry object exposes `contributors` and the template iterates
`entry.contributors`. The template renders serialized data, so the
mismatch meant DRF looked for a `credits` attribute on the entry, found
none, and skipped the field: `credits` is read_only and not required, so
get_attribute raises SkipField and the key is omitted with no error.
`contributors` was never declared and so never serialized, leaving the
template's contributor loop iterating nothing.
The result is that comic credits which are not writing credits -- artists,
colorists, letterers, everyone outside AUTHOR_ROLES -- have never appeared
in a v1 feed. `<author>` was unaffected because all three layers agree on
that name. Atom allows zero or more `atom:contributor` in an entry, so the
schema tests could not catch it either.
Rename the field to match the entry property and the template. The payload
shape was already correct: get_credit_people and its batched variant return
objects with .name and .url, exactly what OPDS1CreditSerializer expects.
Adds tests/test_opds_contributors.py, which seeds a Writer and a Colorist
and asserts each lands in its own element. Without the rename the author
assertion still passes and the contributor one fails, which is the shape
of the bug.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): recognize Panels as a facet capable client (#812)
Panels on iOS added OPDS facet and sort support in 3.13.0, so add it to
UserAgentNames.FACET_SUPPORT. Its CFNetwork style UA parses to "Panels"
through get_user_agent_name, so the existing exact-name match works.
Two more fixes came out of testing that allowlist.
Facet capable clients received the facets twice. The gate in the v1 feed
`entries` property had been commented out, so facets() ran unconditionally
and its OPDS1Link objects were appended to the entries list alongside the
real facet links from _links_facets. OPDS1TemplateEntrySerializer drops
every field those links don't have, so each one rendered as a dead entry:
empty id, no links, unclickable. Restore the gate so the fake navigation
folders are emitted only for clients that can't read facets.
The opds:facetGroup attribute carried internal query parameter names, and
clients like Panels show them verbatim as filter menu headings. Add a
display_name to the FacetGroup dataclass and emit that instead, so the
headings read "Order By", "Order Direction" and "Views". The query param
still drives hrefs and active-facet detection. facet_group also becomes a
CharField; it was typed as a collection-name ChoiceField whose choices
never included any value actually assigned to it.
Adds tests/test_opds_user_agent.py covering both facet variants, the
display names, and User-Agent parsing. Each test fails without its
corresponding fix.
Fixes #810
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* bump version to 2.2.9
* update deps
* memoize use facets order
* fix(opds): gate Panels facet support on build number (#815)
Panels' facet support is per platform and the platforms share one UA
name: the macOS build (951) does not render OPDS facets while iOS builds
(952 and later) do. Matching FACET_SUPPORT on the name alone sent macOS
Panels facet links it can't render, and with no fake nav folders either
it had no sort UI at all.
get_user_agent_name now also returns a build number, parsed from the
token right after the first slash for clients listed in _BUILD_UA_NAMES
(Panels only today). The auth mixin memoizes the pair and exposes it as
user_agent_name and user_agent_build, so existing name consumers are
unchanged. use_facets requires the client's build to meet
UserAgentNames.FACET_SUPPORT_MIN_BUILD (Panels: 952) in addition to name
membership; a missing or unparseable build fails the floor, falling back
to the fake nav folder sort that works on every client. Clients without
a floor, like kybooks, are unaffected.
No cache change needed: the response cache already varies on the full
User-Agent header, so builds 951 and 952 key separately.
Updates tests/test_opds_user_agent.py: the iOS constant moves to build
952, a new test pins that build 951 keeps the nav folder sort and gets
no facet links (it fails against the name-only gate), and the parse unit
tests cover the (name, build) pair including unparseable builds.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* Migrate deprecated EMAIL_* settings to Django's MAILERS (#816)
Django 6.1 deprecates the discrete EMAIL_* connection settings
(RemovedInDjango70Warning at django.setup()), and defining MAILERS makes
reading the old names an AttributeError.
- Replace the eight deprecated settings with an EMAIL_CONNECTION_OPTIONS
dict (TOML/env layer, keyed by EmailBackend constructor kwarg) plus a
MAILERS declaration pointing at the DB-aware DBEmailBackend.
- get_email_connection_kwargs / get_email_from_address coalesce the
EmailSettings DB row over EMAIL_CONNECTION_OPTIONS instead of the
removed settings.
- DBEmailBackend defaults its mailer alias so direct construction never
hits the SMTP parent's pre-MAILERS settings fallback.
- The admin test-send view builds the backend directly and calls
send_messages(), dropping deprecated get_connection() and
EmailMessage(connection=...).
- Tests override MAILERS + EMAIL_CONNECTION_OPTIONS instead of
EMAIL_BACKEND/EMAIL_HOST.
Claude-Session: https://claude.ai/code/session_01Bqa2ULBSaSVDwDSMEni1hf
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): denylist facet-blind Panels builds instead of a build floor (#817)
The build >= 952 floor was built on a wrong premise: Panels build
numbers interleave across platforms. Real iOS builds run lower than the
macOS build - 942 (reported in the field) and 950 (issue #810's
reporter) both render facets natively, while macOS 951 does not - so no
floor can separate them, and 952 shut real iOS users out of facets,
handing them the fake nav folder sort instead.
Replace FACET_SUPPORT_MIN_BUILD with FACET_BLIND_BUILDS, a per-client
frozenset of known facet-blind builds (Panels: {951}). use_facets now
refuses only those builds; every other build - unknown and unparseable
ones included - gets facets, which is the pre-gate behavior that worked
on iOS. A future facet-blind macOS build must be added to the set as
discovered; until then it receives facets it ignores, the pre-gate
status quo.
The iOS test constant moves to the field-reported build 942, an
unparseable-build UA joins the facet-capable cases to pin the
facets-by-default behavior, and the macOS 951 test still asserts the
nav folder fallback. The 942 and unparseable cases fail under the old
floor gate and pass under the denylist.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* update deps
* update deps
* update comicbox version with fix
* update deps
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ajslater
added a commit
that referenced
this pull request
Aug 26, 2026
* fix(frontend): send passwordConfirm from profile change-password (#793)
The Profile dialog's self-service password change posted only
oldPassword + password to /api/v4/auth/password/change, but that
endpoint is rest_registration's ChangePasswordView whose serializer
requires password_confirm (camelCased passwordConfirm) — so the
request 400'd with "passwordConfirm field is required".
The dialog already collects and validates passwordConfirm; forward it
in the changePassword payload, matching change-password-dialog.vue and
the register/reset flows. Add a regression test asserting the field is
sent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps and version to v2.1.1. bump news
* test(frontend): work around vitest/valid-expect false positive on expect.any
The @vitest/eslint-plugin valid-expect rule misclassifies expect.any() as
chai's `.any` flag chain and reports "unknown modifier". Disable the rule on
the one nested assertion with a documented reason rather than weakening it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(onlinetag): drop dead effort option; model Metron's fewer requests
comicbox 4.0.5 no longer applies the effort knob to Metron tagging, and
Metron's search is now a flat two-step (series_list + issues_list) that
match mode does not change.
- Remove the vestigial `effort` option (serializer, task, resume params,
and test). It was collected by the API but never passed to comicbox's
OnlineSession.
- Count estimate calls-per-comic per source: Metron a flat 2, Comic Vine
keeps its per-mode 2/3/5. First-match-wins bills the costliest single
source; merge sums per-source calls. Mirrored in the launcher dialog.
- Resume view drops unknown persisted params so a pre-upgrade `effort`
key in the file-based cache can't crash the task rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* trim news
* refactor(onlinetag): derive source list + issue-id parser from comicbox
Two facilities codex hand-synced from comicbox now consume it directly:
- Source names: KNOWN_SOURCES and the task/serializer/frontend default
lists derive from comicbox's canonical SOURCE_NAMES tuple instead of
repeating {"metron","comicvine"} literals in four places. The frontend
gets it through the tagging choices JSON (build-choices), so a new
comicbox source propagates without hand-editing every site.
- Issue-id parsing: the two byte-identical trailing-int regex copies
(stored_id_prepass, explicit_id) collapse into one
issue_id.parse_issue_id built on comicbox's canonical PARSE_COMICVINE_RE.
It honors the real Comic Vine 4-digit long-key rule instead of grabbing
any trailing int; an unrecognized key returns None, which safely falls
back to search / rejects the id rather than guessing wrong.
No user-visible behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(onlinetag): scope the match-mode request-count hint to Comic Vine
The "~N requests/comic" tail in the match-mode hints only describes Comic
Vine, whose calls scale with match mode; Metron is a flat two-step search
regardless of mode. Drop the tail from the base hints and append a
"~N Comic Vine requests/comic" suffix only when Comic Vine is an active
source, so a Metron-only run no longer shows a count that doesn't apply.
The number derives from the existing COMICVINE_CALLS_BY_MODE constant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(fs): don't let one unreadable folder crash the library scan
The poller's DiskSnapshot._walk() called os.scandir() with no guard
around the directory open, so a single permission-denied folder (e.g.
a Synology /comics/#recycle bin) raised PermissionError that propagated
up and killed the LibraryPollerThread, aborting the scan of every other
folder (issue #795).
- Wrap os.scandir so an unreadable/vanished directory is logged and
skipped instead of aborting the whole poll, and widen the per-entry
guard to cover entry.is_dir(), which can also raise PermissionError.
This matches the os.walk default-onerror behavior the watcher relies
on.
- Register the OS/NAS metadata basenames the filters module already
documented but never populated (@eaDir, #recycle, __MACOSX,
Thumbs.db, desktop.ini), so the walker skips the recycle bin entirely
and NAS/OS junk never enters the library.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* update version to 2.1.2
* fix typechecking
* news for v2.1.2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* format
* Squashed commit of the following:
commit 9db6fe273622635700defb5c9015bf540630e40a
Merge: c8984b9ed ed38cb555
Author: AJ Slater <aj@slater.net>
Date: Sat Jul 4 15:57:53 2026 -0700
Merge branch 'develop' into online-estimate-consume-comicbox
commit c8984b9ede16f82f398501adb49c58d5428c168d
Merge: 2b2e63a35 cd84ed99e
Author: AJ Slater <aj@slater.net>
Date: Sat Jul 4 13:22:52 2026 -0700
Merge branch 'develop' into online-estimate-consume-comicbox
commit 2b2e63a35013de0cd5593c8c5cadb360ffbd23ab
Author: AJ Slater <aj@slater.net>
Date: Fri Jul 3 20:36:09 2026 -0700
feat(onlinetag): consume comicbox 4.1.0 estimate; drop the codex copy
Pin comicbox ~=4.1.0 and move the online-tag run estimate onto its
comicbox.online_estimate.estimate_run() home:
- estimate.py becomes a thin seam over comicbox: estimate_seconds()
forwards to estimate_run().seconds and re-exports SOURCE_RATE_PER_MINUTE.
The request/rate constants and math are deleted -- comicbox owns and
tests them now.
- The launcher dialog's per-source rates and per-comic request model derive
from comicbox via a new tagging-estimate.json (choices/onlinetag.py,
build-choices); only display labels stay in the component, so the JS
estimate can no longer drift from the backend.
- The codex estimate test slims to an adapter / re-export guard.
Prep branch: the ~=4.1.0 pin does not resolve until comicbox 4.1.0 is
published, so uv.lock is untouched and CI targets that shell out to `uv`
will fail until then. Post-publish, run `uv lock`; the change was validated
locally with the 4.1.0 modules installed into the venv.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* refactor(onlinetag): extract resume-param sanitizer to cut complexity
AdminOnlineTagResumeView.post crossed radon's C threshold once the resume
descriptor sanitization landed. Move that logic (sources tuple coercion +
dropping keys no task field accepts) into a module-level helper; the view
falls to rank B and reads more directly. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(onlinetag): reference defined constant in match-mode hint
matchModeHint read an undefined COMICVINE_CALLS_BY_MODE, throwing a
ReferenceError on every launcher-dialog render (and failing
tests/unit/launcher-dialog.test.js). Point at the real
TAGGING_ESTIMATE.comicvineRequestsByMode map that callsForSource
already uses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(settings): nest comicbox loglevel/delete_keys under general section
An un-nested overlay is silently ignored by confuse.
* fix(onlinetag): resolve prompts against current DB path, surface apply failures
Answering a deferred prompt fetched the chosen issue against the path
serialized into the prompt at scan time. When an earlier write for the
same comic ran with rename enabled (the comic's other source's prompt,
or a stored-id prefetch), that path was stale and the apply died with an
uncaught FileNotFoundError — after the prompt was already consumed, so
the admin's pick vanished with no feedback.
- _apply_resolution now re-reads the comic's path from the DB by pk;
a missing row reports to the Tagging error panel instead of fetching
a dead path.
- fetch/replay failures (ComicboxError, OSError) and non-resolving
explicit ids now land on the Tagging error panel instead of only the
log, since the pick can no longer be re-prompted.
- stored-id prefetch and tag_by_id also catch OSError so a vanished
file degrades gracefully.
- regression test for the COMICBOX_CONFIG general-section overlay
(un-nested loglevel/delete_keys were silently ignored, letting
comicfn2dict remainders like "(0000)" leak into rename targets).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps and format
* Native OIDC single sign-on (Admin Auth tab) (#798)
* feat(auth): native OIDC login via django-allauth
Codex becomes an OIDC Relying Party (Authentik/Authelia) with a
config-gated login flow:
- [auth.oidc] TOML section + CODEX_AUTH_OIDC_* env overrides
- allauth apps installed unconditionally; behavior gated on
AUTH_OIDC_ENABLED (all OIDC paths 404 when off)
- CodexSocialAccountAdapter: username linking (superusers included,
documented trust boundary), optional email linking, claim-chain
username mapping with sub-hash collision suffix, groups-claim sync
to existing Django groups, admin-group grant/revoke, error
redirects to the SPA (never an allauth template)
- Branded throttled init endpoint /api/v4/auth/oidc/login; allauth
login/callback mounted at /sso/ (outside the namespaced API tree so
allauth's internal reverses work)
- RP-initiated logout URL via cached discovery document using the
spec's client_id parameter (no stored tokens needed)
- /session payload gains public oidcEnabled/oidcProviderName/
oidcLoginUrl and authenticated oidcLogoutUrl
- Profile username locks per-user when an OIDC identity is linked
- OIDC failures reuse the failed-login log line format for fail2ban
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): SSO login button, RP logout, and sso-error page
- auth store: oidc admin flags, loginSSO() full-page navigation,
logout() follows oidcLogoutUrl for RP-initiated logout
- SsoLoginButton shared by the login dialog (with divider) and the
unauthorized lock screen
- /auth/sso-error route + page mapping backend error codes to human
messages, with retry hidden for non-retryable codes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(auth): OIDC setup guide + complete tinyauth forward-auth recipe
- README: native OIDC section (config table, redirect URI with prefix,
Authentik/Authelia walkthroughs, identity-mapping and admin-linking
trust warning, session-lifetime and OPDS caveats)
- README: full nginx auth_request recipe for tinyauth with header
override hardening, Traefik/Caddy equivalents, and a forward-auth
deployment checklist (OPDS + WebSocket gating, spoof test)
- schema test: allauth views stay out of the OpenAPI schema
- test typing fixes surfaced by basedpyright
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* fix(settings): nest comicbox loglevel/delete_keys under general section
An un-nested overlay is silently ignored by confuse.
* fix(onlinetag): resolve prompts against current DB path, surface apply failures
Answering a deferred prompt fetched the chosen issue against the path
serialized into the prompt at scan time. When an earlier write for the
same comic ran with rename enabled (the comic's other source's prompt,
or a stored-id prefetch), that path was stale and the apply died with an
uncaught FileNotFoundError — after the prompt was already consumed, so
the admin's pick vanished with no feedback.
- _apply_resolution now re-reads the comic's path from the DB by pk;
a missing row reports to the Tagging error panel instead of fetching
a dead path.
- fetch/replay failures (ComicboxError, OSError) and non-resolving
explicit ids now land on the Tagging error panel instead of only the
log, since the pick can no longer be re-prompted.
- stored-id prefetch and tag_by_id also catch OSError so a vanished
file degrades gracefully.
- regression test for the COMICBOX_CONFIG general-section overlay
(un-nested loglevel/delete_keys were silently ignored, letting
comicfn2dict remainders like "(0000)" leak into rename targets).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(auth): move OIDC config from codex.toml to the Admin UI Auth tab
OIDCSettings DB singleton (EmailSettings pattern) becomes the sole
config source, read at request time:
- OIDCSettings model + migration 0047 (seeds pk=1, one-time courtesy
import of any pre-GUI [auth.oidc] TOML values); client_secret
encrypted at rest via EncryptedCharField
- get_oidc_settings()/oidc_enabled() in settings.db; cachalot makes
admin edits live on the next request, no restart
- codex/oidc.py rewired to request-time reads; new adapter
list_apps override builds an unsaved SocialApp from the row
(per-app settings['scope'] wins in allauth's get_scope), so
disabled state keeps allauth's own DoesNotExist -> 404 gating
- RP-initiated logout and session flags read the row
- AdminOIDCSettingsView GET/PUT (write-only secret + clientSecretSet
mirror, discovery-cache invalidation on save) and AdminOIDCTestView
(discovery-document probe) at /api/v4/admin/oidc-settings[/test]
- New Admin UI Auth tab mirroring the Email tab: draft/dirty
tracking, never-echoed secret with Clear Credential, redirect-URI
display, Test Connection endpoint report
- [auth.oidc] TOML section and CODEX_AUTH_OIDC_* env overrides
removed; README updated
- Tests now seed the DB row instead of patching module constants
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* format
* update deps and fix
* fix(admin): gate the OIDC enable switch on server URL + client ID
The Auth tab's Enable OIDC Login checkbox is disabled until a valid
server URL and a client ID are entered (it can always be unchecked so
clearing a field never strands the switch). The serializer enforces
the same invariant for API clients and partial updates that blank a
prerequisite while enabled — previously such a save produced a
silently inert enabled=true row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(admin): Auth tab gains Account & Access flags, tinyauth note, name gate
- Move the Account & Access flag cards (Registration, Verify New User
Email, Non-Users) from the Users tab to the Auth tab — they govern
how people get in, which is that tab's subject
- Auth tab prose explains that forward-auth gateways like tinyauth are
not OIDC providers and points them at Remote-User header auth, which
coexists with OIDC
- Provider name joins server URL and client ID as an enable
prerequisite, in the UI switch gate and the serializer invariant
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(admin): visually nest the OIDC subsections under their header
AdminSection gains a sub variant: a small uppercase overline title (h4,
$text-meta) and an indented left rule, with tighter sibling rhythm than
top-level sections. The Auth tab wraps the whole OIDC block — prose,
Identity Provider, User Mapping, Logout, and Test Connection — in one
parent 'OIDC Single Sign-On' AdminSection with the config groups as sub
sections, so their subordination to the OIDC header is unmistakable
next to the sibling Account & Access section. Documented in DESIGN.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(admin): collapse the OIDC section when OIDC is disabled
Most admins never configure OIDC, so the section body — prose, config
sub-sections, and Test Connection — hides behind an AdminExpandToggle
disclosure. It starts expanded only when OIDC is already enabled;
otherwise a one-line hint summarizes what's inside next to a Configure
toggle. The disclosure is initialized once from the saved state so
saving a disable doesn't slam the panel shut mid-edit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(admin): plain-English hints for PKCE and other OIDC jargon fields
PKCE, Client ID, Username Claim, and Groups Claim now carry hints an
admin who has never touched OIDC can act on — including what a claim
is and why PKCE should stay on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): authentik + tinyauth manual test harness in test-proxy/
Adds a docker-compose IdP stack and nginx wiring so SSO can be manually
verified before release:
- compose.yaml: authentik (OIDC provider, :9010) + tinyauth (forward
auth, :3232), everything bound to localhost with throwaway creds
- authentik/blueprints/codex-test.yaml: auto-applied fixtures — readers
and codex-admins groups, testuser/testadmin, and the codex-test OIDC
client with callback URIs for proxied and direct, prefixed and bare
- forwardauth.conf: nginx :8081 gating Codex behind tinyauth
auth_request with an overriding Remote-User header
- README.md: step-by-step test matrix covering native OIDC (login,
group sync, admin mapping, RP logout, linking, error page, disabled
404) and forward-auth (login, gating, spoof-proofing, coexistence)
tinyauth DB path pinned to the writable /data volume (workdir is
root-owned). test-proxy/ excluded from eslint: authentik !Find tags and
compose healthcheck arrays require flow-style YAML the yml plugin bans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): run the harness nginx as a compose service
nginx joins authentik + tinyauth in compose.yaml, so only Codex runs on
the host. The native bin/run-test-proxy.sh path still works — both share
server.conf/forwardauth.conf, with the sole native-vs-container
difference (backend addresses) isolated into named upstreams:
- upstreams-native.conf: localhost backends (host nginx)
- upstreams-docker.conf: host.docker.internal + tinyauth service name
- connection-upgrade.conf: the ws-upgrade map, now shared
- ssl-listen.conf / ssl-listen-none.conf: SSL/QUIC listeners split out so
the container serves plain HTTP (native keeps the 8443 listeners)
The compose nginx mounts these into the stock image's conf.d and reaches
host Codex via host.docker.internal (extra_hosts host-gateway for Linux).
Also fixes a latent harness bug that would break OIDC through the proxy:
X-Forwarded-Host used $host (strips the port), so Django's
build_absolute_uri produced a portless redirect_uri that couldn't match
the registered callback. Now $http_host, port included.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): fix tinyauth boot crash on localhost app URL
tinyauth v5 derives a cookie domain from its app URL at startup and
rejects single-label hosts and IPs ('invalid app url, must be at least
second level domain'), so http://localhost:3232 crash-looped. Move the
forward-auth path onto *.localtest.me (all subdomains resolve to
127.0.0.1 via public DNS, every browser, no /etc/hosts):
- tinyauth app url -> http://tinyauth.localtest.me:3232
- gated Codex -> http://codex.localtest.me:8081
- shared cookie -> .localtest.me (spans both)
The @tinyauth_login redirect and README Test 2 follow. OIDC/authentik
stay on localhost (no cross-host cookie needed there).
Also documents in README Troubleshooting that the harness publishes only
9010/8080/8081/3232 and never binds Vite's 5173 — a blocked HMR is a
stale vite process, and 8080/8081 clashes come from running native
make dev-reverse-proxy alongside the compose nginx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sso): set authentik provider grant_types; drop IPv6 host-gateway
Two issues from the first live OIDC run:
- 'Login with Authentik' failed with authentik logging 'Invalid
grant_type for provider'. authentik 2026.x added an explicit
grant_types model field that defaults to an EMPTY list, so a blueprint
that omits it creates a provider allowing no grants and the authorize
step returns invalid_request. Set grant_types: [authorization_code,
refresh_token] on the provider.
- nginx logged 'connect() to [fd..::254]:9810 Network unreachable' then
fell back to IPv4. The IPv6 came from extra_hosts host-gateway (a
Docker Desktop IPv6 ULA gateway Granian doesn't listen on). Comment it
out — Docker Desktop provides an IPv4 host.docker.internal built-in;
Linux users uncomment it.
README troubleshooting covers both, including re-applying the blueprint
to an already-running authentik.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auth): refresh public flags when OIDC is toggled or on logout
The 'Login with <provider>' button (adminFlags.oidcEnabled) went stale
after disabling OIDC: OIDCSettings is a singleton with no
admin.flags.changed websocket broadcast, and logout() left adminFlags
untouched, so the button lingered on the login screen until a manual
page reload.
- admin.updateOidcSettings now calls auth.loadAdminFlags after a save,
resyncing the public OIDC flags immediately.
- auth.logout now re-fetches public flags (except when doing an
RP-initiated full-page redirect, which reloads anyway), so the
logged-out login screen always reflects current settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
* chore(lint): clear radon CC/MI and remark warnings
- codex/oidc.py: extract CodexSocialAccountAdapter._sync_admin from
_sync_user (rank C -> B); keyword-only bool arg for FBT001.
- tests: split the 940-line test_onlinetag_session_manager (MI rank B,
pre-existing on develop) — move the TagPassRunner and stored-id-map
classes into test_onlinetag_tag_pass.py, importing the shared doubles
from the session-manager module (as test_opds_schema already does).
Both files now MI rank A.
- test-proxy/README.md: wrap the bare http://localhost autolink in <>
and the [fd..::254] nginx error in backticks so remark-lint stops
reading it as a link reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* bump version 2.2.0
* bump news
* update deps and comicbox
* adapt ComicVine credential check to simyan v3 (comicbox 4.1.1)
simyan 3.0 removed the cache= constructor kwarg. The credential check
now passes cache_expiry=DO_NOT_CACHE with the cache/ratelimit sqlite
files in a throwaway temp dir, so validation always hits the network
(api_key is excluded from simyan's cache key) and leaves no files
behind. Also note comicbox 4.1.1's ComicVine improvements in the
v2.2.0 news.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* test(onlinetag): assert merge flag is forwarded, not comicbox's arithmetic
test_estimate_seconds_passes_merge_flag pinned a hardcoded 1000.0 that
went stale when comicbox 4.1.1 changed Comic Vine pacing to bill the
busiest resource pool (simyan 3.x per-endpoint buckets) instead of the
request total. The codex seam only forwards to comicbox.estimate_run, so
re-derive nothing here: assert the flag's defining effect — merge sums
every source's pace, so it costs strictly more than first-match-wins —
which proves forwarding without coupling to comicbox's rate model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* update deps
comicbox 4.3.0 (mokkari 4 header-driven Metron rate limits), pinia 4.0.2,
vue-router 5.2, vuetify 4.1.5, vite 8.1.5, eslint plugin updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v2.2.1: show Metron account rate limits live (comicbox 4.3.0 / mokkari 4)
Testing Metron credentials in the Admin panel now reports the account's
real burst and daily limits read off the validation response's
X-RateLimit-* headers — the daily limit reflects the user's Metron donor
tier. The online tagging status table shows the live daily budget as a
run progresses, via comicbox 4.3.0's newly wired
OnlineSession.rate_limit_status().
Also removes a ty ignore in tests/opds_schema.py made stale by the dep
updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* v2.2.1: community ratings replace critical rating (comicbox 4.4.0)
comicbox 4.4.0 remaps ComicInfo CommunityRating / CBI rating — the tags
that fed critical_rating — to its new community_rating field
(average_rating + rating_count, Metron-filled), and critical_rating no
longer persists to any format. Migration 0048 renames the column
(values carry: same tags, same scale), adds community_rating_count and
alternative_issue number/suffix columns, and remaps user settings that
reference the old key (order_by, table_columns JSON) in RunPython.
Community rating gets full browser parity: sort (Avg aggregate), table
column, sidebar filter, and field search incl. rating_count. The
metadata dialog shows '4.2 / 5 (128 ratings)' and the tag editor edits
the pair, with the count enabled for MetronInfo only (the only format
that persists it). Alternative issues import and display ('#43.5AU').
Sidecar backups tolerate the rename both ways: schema column renamed,
restore remaps legacy critical_rating filter columns, order_by values,
and table_columns keys from old dumps.
Also: ty ignores for dep-bump invalid-method-override errors in
vuetify serializer fields; test fixtures rebuilt with deterministic
past-dated zip mtimes (the importer prefilter skips future-dated
embedded mtimes as unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* v2.2.2: fix rotated pdf page serving (comicbox-pdffile 0.6.3)
PDF pages scanned upside down or sideways and righted by the pdf's
rotation attribute displayed rotated when the reader served them as
images; pdffile 0.6.3 re-renders rotated image-dominant pages instead
of serving the stored bytes. Also stops a read-only page serve from
rewriting a pdf on disk when MuPDF repairs its content streams in
memory (close() no longer saves on the repair-dirty flag).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps, including comicbox 4.5.0
* Fix silent no-op when clearing tag editor fields
Clearing a field encoded the clear as an empty patch value (""/{}/null),
but comicbox prunes empty values on schema load and a merge write can
only add or replace — so every "clear field" action was a silent no-op
on the archive.
Cleared and emptied fields now travel as comicbox delete_keys glom paths
(new in comicbox 4.5.0): buildPatch returns {patch, deleteKeys} ->
tag-write and preflight POSTs -> serializer -> BulkTagWriteTask ->
BulkWriteItem. A clear-only edit sends an empty patch and still writes.
Rename previews layer the delete keys onto the preview config so a
cleared series or issue drops out of the previewed filename too.
Also document why the read-side COMICBOX_CONFIG must never become the
write base config: comicbox unions a write's delete_keys with the base
config's, and the read config skips every schema field codex doesn't
consume, so using it would strip all of them from the user's archive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix lint errors from the ruff 0.16 upgrade
The dependency update to ruff 0.16.0 enabled rules that flag four
pre-existing spots, failing lint (and CI) independently of any code
change: RUF036 None-last in two exception handler unions, PLC0206 dict
iteration without .items(), and PLR0917 too many positional arguments.
The mail backend's positional signature mirrors Django's
SMTPBackend.__init__ to stay a drop-in, so PLR0917 joins the PLR0913
suppression already there rather than changing the signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update comicbox
* Clear monochrome by deleting the tag, not writing false
comicbox 4.5.1 fixes ComicInfo's BlackAndWhite in both directions, so
monochrome is now a real tri-state tag: Yes, No, or absent. The clear
icon set the patch value to false, which now writes <BlackAndWhite>No
</BlackAndWhite> — asserting the comic is known to be color instead of
removing the tag. The bug was invisible before 4.5.1 because nothing
was written at all.
Cleared monochrome joins the other cleared fields in delete_keys;
explicitly unchecking the box still patches a false.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update devenv and deps
* update claude rules about telemetry
* Fix anonymous stats sending and report what codex grew into
v2.2.3. Two problems: stats had not been sent by any install since v2.0.0,
and what they would have sent no longer described codex.
Transport. Replacing requests with urllib left the server credentials in the
URL. urllib hands the whole netloc to http.client, which reads the password
as a port and raises InvalidURL, so every send failed before opening a
socket. A leftover requests-style raise_for_status() would have broken a
successful send too. Both were logged at debug, which is why this went
unnoticed for months; failures now log at warning. Credentials move to an
Authorization header, and CODEX_TELEMETER_URL points a dev install at a local
chronicle.
Payload, now wire version 2. Adds counts and settings for everything since
v1.12: read-only libraries, custom covers, favorites, bookmarks, community
ratings, browser table view, online tagging, single sign on, email, rate
limits and reverse-proxy deployment. Identifiers are counted by source and
type, which is the only durable measure of online tagging use.
It stays counts, booleans and closed-enum values, per the telemetry privacy
rule. Nothing an administrator typed is included: no paths, account or group
names, search terms, banner text, service URLs, API keys or credentials. A
setting holding text or a secret reports only whether it is set. Identifier
source names come from comic files, so they are mapped through comicbox's
known sources and anything else becomes "other" before it leaves the process.
tests/test_telemeter_privacy.py seeds a sentinel into every such field and
fails if one reaches the payload, then requires every value to be a number, a
boolean, or a string from a closed vocabulary.
The Admin Stats tab renders every new section, so the page still shows the
whole report. Two label fixes there: the metadata table pluralizes by
appending "s", which produced "Storys" and "Comic Metadata Importeds".
Also: the identifiers section never reached the admin endpoint.
AdminStatsRequestSerializer fills in every section it declares, so params is
always truthy and the section gate drops anything undeclared - an empty table
with no error. Declared it, and added a test so the next section added to
StatsSerializer cannot vanish the same way. FILE_TYPES_CHOICES was missing
CB7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* Support comicbox 4.6.0 series alternative names
comicbox 4.6.0 records a series' localized and variant titles as
reprints: from MetronInfo AlternativeNames and Reprints tags, and now
from Metron and Comic Vine online tagging. Codex dropped the field
entirely -- reprints sat outside USED_COMICBOX_FIELDS, so the read
config told comicbox to skip parsing it and the aggregator popped it as
a backstop.
Import them into a new denormalized Reprint model keyed on series name,
volume number, issue and language. Series and Volume are browse
collections, so reusing them would have hung phantom rows off the
browser for every alternate title. A reprint may carry only a series
sort_name, which stands in when the name is absent; one with neither is
dropped.
Alternate names reach the metadata panel, the tag editor, a sortable
browser column and filter, and a distinct alternate_series FTS column,
so a comic filed under a localized title is findable by it. The column
is distinct rather than folded into series to keep the series: token
exact; unqualified searches match either.
Online matching now recognizes Comic Vine volume aliases, which codex
inherits by delegating matching to comicbox. The match prompt shows
each candidate's alternative names, since a comic filed under a
localized title matching its canonical volume otherwise reads as a
wrong match, and passes the chosen candidate's volume id, which was
plumbed end to end but always null.
Drop the auto_threshold setting. It was parsed, serialized and
round-tripped through resume, but never reached OnlineSession, which
accepts no threshold.
Reconcile sidecar columns on open. schema.sql is all CREATE TABLE IF
NOT EXISTS, so a sidecar predating a release never gained its new
settings_filters columns and the next dump failed with "no such
column". The wanted columns derive from the schema file rather than a
list that would rot the same way. This also repairs the column the
community_rating rename added.
Already-imported comics pick alternate names up on a Force Update Tags;
adding a field to the whitelist does not invalidate metadata_mtime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps and devenv
* bump news for comicbox 4.6.1
* update devenv
* fix cron double enqueue of telemeter task
* test crond double enqueu fix
* test telmemeter logging fixes
* feat(api): reintroduce Swagger UI at /api/v4/
The interactive docs were lost in the v4 cutover (9b8314d3b, v2.0.0)
when codex/urls/api/v3.py was deleted; only the raw schema route
survived. Mount SpectacularSwaggerSplitView at the v4 root, gated on
FEATURES.swagger — the flag that until now only switched a CSP overlay
for routes that no longer existed.
The split view keeps its init javascript in a second same-origin
request, so no inline <script> needs a nonce.
Also fix the CSP overlay it depends on: it listed the jsdelivr bundles
under script-src only, but the pdfs-dist overlay declares
script-src-elem, which masks the script-src fallback for element loads
and would have blocked both bundles. List them under both directives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* v2.2.4
* sub api v3 for v4
* feat(tagging): authenticate to Metron with an API key
Metron issues API tokens now, so the admin Tagging tab offers a single
API Key field and no longer mentions usernames or passwords. The key
lands in a new encrypted metron_key column (migration 0050) and reaches
comicbox as OnlineCredentials.metron_key, which mokkari uses as its
Bearer token. Preferring the key over a login needs no code: mokkari
drops basic auth whenever a token is present.
Logins saved before this release keep working. Every path that decides
whether Metron is configured -- the scan session, tag-by-id, credential
testing, and the telemetry boolean -- accepts a key or a username and
password pair, matching comicbox's own is_configured. The validator
passes api_token=None rather than "" so an absent key doesn't send an
empty Bearer header and defeat that fallback.
Writing the key retires the login it replaces: a PUT carrying
metron_key blanks metron_user and metron_password, so saving a key or
clearing credentials both leave no stale login behind. A save that
omits the field (a custom-URL edit, the settings auto-save) leaves a
stored login alone.
The user-data sidecar exports metron_key too, and restore skips
coalesced columns a backup predates -- sqlite3.Row raises on a missing
column, so an older sidecar would otherwise crash the restore.
Requires comicbox 4.7.1, which also warns once per process when basic
auth is what actually gets used.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* feat(tagging): remove the custom URL fields for Metron & Comic Vine
The Metron custom URL never did anything: mokkari hardcodes
METRON_URL = "https://metron.cloud/api/{}/" and exposes only dev_mode as
an alternative, so comicbox's metron source drops the credential url and
warns that it is a no-op. The Comic Vine override did work via simyan's
base_url, but a comic server has no use for pointing at a different
Comic Vine.
Removes both fields from the model (migration 0051), the admin and
validate serializers, the validate view's credential tuple, all three
librarian consumers, the two telemetry booleans and their stats
serializer fields, and the user_data backup/restore path. comicbox keeps
its OnlineCredentials url fields; codex just stops passing them, so
test_online_credentials_fields_stable still expects them.
Old sidecar backups carrying the dropped columns restore fine: the
restore comprehension walks its own allowlist rather than the row, the
same way it already ignores the retired active_session_id.
Also fixes a bug from 8fbf7e11f in the same DDL: schema.sql never gained
a metron_key column, so the tagging_defaults upsert failed with "no such
column" — and since _dump_queryset only logs per-row failures, every
backup silently wrote zero tagging rows. Adds the column (_reconcile_
columns retrofits existing sidecars) and a dump round-trip test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): clear the two outstanding ty errors
test_snapshot_diff: a real annotation bug. The ``_snapshot`` helper took
``models: dict[str, type]`` but assigns it straight to
``Snapshot._path_to_model``, which is ``dict[str, type[Model]]``. All
five call sites already pass Comic or Folder, so the parameter was just
looser than both its callers and its destination.
test_bookmark_filter_isolation: unavoidable suppression. The mixin
declares ``self.request: Request`` and the helper deliberately assigns a
SimpleNamespace, since its whole point is to skip DRF's request
lifecycle. A ``cast`` traded the ty error for basedpyright's
reportInvalidCast (the types don't overlap), and its suggested
double-cast through ``object`` reads worse without making the stub any
more of a Request. Adds ``# ty: ignore[invalid-assignment]`` beside the
existing pyright ignore, per the two-checker convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tagging): present Metron as an API key source & warn on legacy logins
The admin Tagging tab called Metron's credential a "credential" while Comic
Vine's twin panel called it an API key. Metron authenticates with a
metron.cloud API key now, so say so — except on an install whose only stored
credential is still the legacy username & password.
- Metron's status, clear button, and confirm dialog switch between an API-key
and a legacy-credential label set, keyed on metronKeySet.
- A legacy-only install gets a warning-colored deprecation notice linking to
Metron's token authentication announcement, and the panel opens itself so
the notice isn't buried behind a click.
- The Online Tagging dialog carries a one-line version of the same warning,
shown only when a legacy login is stored AND this session would query
Metron (selected on the Search tab, or the id's source on the By ID tab).
- Comic Vine's save button and the source-disabled tooltip say API key too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update devenv
* update deps
* fix(importer): import a renamed comic's new tags instead of failing
Tagging a comic with rename enabled produced a failed import for the
pre-rename path, and the tags that had just been written never imported
at all.
The tag write and the rename land in one watch batch: a modify naming
the old path, plus a delete+add that inode matching pairs into a move.
Move detection never looked at the modify, and the task builder pruned
modified paths only against move destinations, so the task carried
files_moved={old: new} alongside files_modified={old}. The importer
applies moves before reading, so the read opened a path that no longer
existed.
Remap modified paths through the move map rather than dropping
destinations. Sources become their destination (the write-then-rename
every external tagger performs, codex's own included), and destinations
survive, which the poller emits deliberately for a move whose stats
also changed.
Stop the move phase from refreshing Comic.stat. The stored stat means
"the file as of its last tag import", so refreshing it on a move erased
the only evidence the read phase had that the renamed file's contents
had changed too -- the tags were lost rather than deferred. A pure
rename leaves inode, mtime and size alone, so the preserved stat still
matches disk.
Also stop recording failed imports for files that vanished mid-import:
the row was queued before presave() stat'd the path, so the OSError
meant to drop it did not. Key the failed-import map by str so its
membership test against db paths can match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v2.2.5
* feat(tagging): combine online search and tag-by-id into one pass
The Tag Online dialog's Search and By ID tabs become one pane. Ids are
pinned per source: a pinned source is fetched by that issue id while the
unpinned ones search, in a single comicbox lookup, so merge_all_sources
merges across both. The submit button reads Search, Tag by ID & Search,
or Tag by ID accordingly, and entering an id selects its source.
Ids now ride on tag-sessions/start as {source: token}. That retires the
parallel POST /admin/tag-by-id path entirely -- AdminTagByIdView,
OnlineTagByIdTask, TagByIdRequestSerializer -- so tagging by id gains
session status, resume, and the write pipeline the scan already had.
Also drops dry_run, which the start view accepted and never read.
run_session skips the DB stored-id prepass when ids are pinned: the
prepass pops the comic out of comic_paths, which would leave the
unpinned sources nothing to search.
Needs comicbox 4.8.0 for OnlineSession(ids=...). That release is not on
PyPI yet, so the pin here is still ~=4.7.1 -- bump it after publishing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* update deps
* style(news): prettier wrap the online tagging entry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tagging): restore the custom URL for Comic Vine
v2.2.4 (0dd3e5199) removed the custom URL fields for both online
sources. Metron's was correctly retired — mokkari hardcodes
METRON_URL and comicbox warns the url is a no-op — but Comic Vine's
worked: comicbox passes it through OnlineCredentials.comicvine_url to
OnlineSourceCredentials.url and on to simyan's base_url, which is what
lets tagging run against a Comic Vine proxy or mirror.
Restores the Comic Vine half only, no comicbox change needed:
- Model field + migration 0052. A plain URLField rather than an
EncryptedCharField, because unlike the API keys it is not a secret
and must read back for the admin form's placeholder.
- Admin serializer (read+write), validate request serializer, and the
validate view's _CREDENTIAL_FIELDS.
- All three librarian consumers: the scan session's OnlineCredentials,
the explicit-id auth mapping, and the credential validator's simyan
base_url, so Test checks the endpoint the scan will actually use.
- Backup/restore sidecar column, serializer, and restore allowlist.
_reconcile_columns retrofits existing sidecar files; a legacy
metron_url column in an older backup stays silently ignored.
- Telemetry reports only bool(comicvine_url) — never the value.
- The admin Tagging tab's Comic Vine panel gets the field back, its
save button reverts to "Save Comic Vine Credentials" now that a
URL-only save is possible again, and Clear removes both.
A URL alone is not a credential: it must not satisfy the session
manager's configured-sources gate, enable the source checkbox, or pass
validation without a key. Tests pin all three.
URLs dropped by 0051 are unrecoverable; admins re-enter them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* bump version and update deps
* bump dockerfile source version
* granian 2.8.0
* update deps
* update devenv
* update devenv and deps
* use nodejs26 in the builder
* update deps
* remove old ty ignore
* update deps
* fix radon complexity warnings
Three items `make complexity` flagged, all pure refactors.
AdminOnlineTagStartView.post (CC 11 -> C): three near-identical
"request value or stored default" blocks collapse into a
_FLAG_DEFAULT_FIELDS mapping plus a _resolve_flags helper.
test_telemeter_privacy._vocabularies (CC 12 -> C): split by vocabulary
source into _choice_vocabularies, _identifier_bucket_vocabulary and
_tagging_vocabularies, with the literal and OIDC sets hoisted to module
constants. The same string set is checked; the walk is not widened.
test_onlinetag_session_manager (MI 17.37 -> B): 916 lines in one module,
every function already rank A. Split along its seams into the scan pass,
prompt resolution and credentials, with the doubles and the comic factory
moved to tests/onlinetag_session_fakes.py. All 31 tests come across
unchanged; the move dedupes the BulkTagWriteTask filter into
write_tasks() and the prompt dict into a _prompt() factory.
test_onlinetag_tag_pass now imports the fakes from their new home.
make complexity, lint, ty clean; 815 pytest + 371 vitest pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update deps
* fix(importer): skip comic moves onto claimed paths
A move whose destination path already belonged to another comic row
violated the (library, path) unique constraint inside bulk_update. The
IntegrityError aborted the whole import before the delete phase could
clear the stale row, so the poller rebuilt the same task and crashed on
every subsequent scan.
Mirror the folder guard for comics and custom covers: drop moves onto
paths an existing row holds, and moves that two sources in one batch
claim, which files_moved can express because it is a plain dict rather
than a bidict. Skipping converges. The destination row's stat refreshes,
the next scan sees two rows sharing an inode and suppresses the bogus
move, and the stale source falls through to the delete phase.
Also contain any move phase failure in all three move steps so a bad
batch degrades to a skipped phase the next scan reconciles instead of
aborting the import. This covers the same class of crash in the folder
step, where converting dirs_moved to a bidict raises on the duplicate
destinations the watcher can emit.
Fixes #807
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* bump version
* update deps
* update deps
* fix(update): make codex self-update work on macos and linux
The self-update was not flaky, it was inert.
Nothing ever asked for an automatic update. update_latest_version()
had an `update` hook that queues JanitorCodexUpdateTask and no caller
passed it, and the update task was not in the nightly fan-out, so the
Auto Update flag promised a daily upgrade that could never happen. The
nightly check now forces a fetch and chains into the update when the
flag is on. The flag gate moves to that scheduling point, so the admin
Jobs tab button updates whether or not the flag is set -- it used to
silently no-op, since the flag defaults off and the button sends
force=False.
The installer ran `sys.executable -m pip install --upgrade codex` with
no timeout, no captured output, and a blanket except that swallowed
every failure while still logging "updated to the same version". uv and
pipx environments have no pip and the docker image uninstalls it, so
those installs always failed invisibly. Now: pip if importable, else
`uv pip install --python <sys.executable>`, else an ERROR naming both
and pointing docker at a new image. Failures log the installer's own
stderr, a hung installer times out instead of wedging the scribe queue
behind it, and the restart only fires when the install really happened.
Restarting exec'd __file__, which relied on run.py's executable bit and
its `#!/usr/bin/env python3` shebang resolving to a python that has
codex installed. Under macOS, pipx, uv, systemd and launchd that is
frequently a different interpreter and the server never came back. Exec
`sys.executable -m codex.run` instead.
_is_outdated() called Version() on unvalidated strings, raising on a
fresh install's empty cache and on a source checkout's "test" version.
Version comparison is now codex.version.is_outdated(), total by
construction, shared by the janitor and the version view.
The browser could not announce anything: semverGreaterThan(a > b) passed
one boolean into a two-argument function so `outdated` was always false,
and the comparison read 1.0.9 as newer than 1.1.0. The server ships
`outdated` and `docker` in the version payload and the javascript
comparison is gone. The footer renders a codex orange "upgrade to codex
vX.Y.Z" link to the Update Codex job, or to the image repo in docker,
where codex cannot install over itself. The Jobs tab grows section
anchors, names the version the job would install, and explains the
docker case.
While the cache is empty every /api/v4/version request queued a fetch
task, each on its own thread with a 5 second PyPI call. Add a process
wide in-flight lock and a cooldown after a failed fetch.
Tests cover the version comparison, the payload, installer selection and
failure paths, the flag gate and fetch guards, the nightly wiring, and
both frontend surfaces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(proxy): document proxy_pass_header Server for nginx
nginx hides the upstream Server header by default (along with Date,
X-Pad and X-Accel-*) and substitutes its own, so codex/<version> never
reaches clients behind a reverse proxy. Granian passes the header set by
CodexMiddleware through untouched; nginx is the only clobberer.
Add proxy_pass_header Server to the README's example location, plus a
subsection covering the curl verification, why server_tokens off is not
a substitute, and the array-directive inheritance trap (a location that
declares any proxy_pass_header drops the one inherited from server{}).
Set the same directive in the test-proxy harness so the documented
config is the one actually exercised.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* update new for docs too
* update deps
* update deps
* fix(opds): vary the response cache on User-Agent (#813)
OPDS responses were cached without varying on User-Agent while the body
depends on it, so a client could be served a variant rendered for a
different one until the entry expired. UserAgentNames switches facet
emission (FACET_SUPPORT), download mime types (SIMPLE_DOWNLOAD_MIME_TYPES),
order facet suppression (CLIENT_REORDERS) and absolute hrefs
(REQUIRE_ABSOLUTE_URL), and cache_page keys only on the URL plus the
headers named in Vary.
Add User-Agent to the existing vary_on_headers in opds_cached, which
covers every wrapped v1 and v2 feed, start, manifest and opensearch
route. Vary already includes Cookie, so per-session keys existed anyway
and this barely fragments the cache further; it also corrects what
intermediary caches are told. Cover routes keep the narrower vary since
covers don't depend on the client, and the static authentication
document is left alone.
Adds tests/test_opds_cache.py asserting the Vary header names User-Agent
and that a feed primed by one client isn't replayed to another. Both
fail without the fix.
Fixes #811
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): send v1 entry contributors, which were silently dropped (#814)
OPDS1TemplateEntrySerializer declared the field as `credits`, but the
entry object exposes `contributors` and the template iterates
`entry.contributors`. The template renders serialized data, so the
mismatch meant DRF looked for a `credits` attribute on the entry, found
none, and skipped the field: `credits` is read_only and not required, so
get_attribute raises SkipField and the key is omitted with no error.
`contributors` was never declared and so never serialized, leaving the
template's contributor loop iterating nothing.
The result is that comic credits which are not writing credits -- artists,
colorists, letterers, everyone outside AUTHOR_ROLES -- have never appeared
in a v1 feed. `<author>` was unaffected because all three layers agree on
that name. Atom allows zero or more `atom:contributor` in an entry, so the
schema tests could not catch it either.
Rename the field to match the entry property and the template. The payload
shape was already correct: get_credit_people and its batched variant return
objects with .name and .url, exactly what OPDS1CreditSerializer expects.
Adds tests/test_opds_contributors.py, which seeds a Writer and a Colorist
and asserts each lands in its own element. Without the rename the author
assertion still passes and the contributor one fails, which is the shape
of the bug.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): recognize Panels as a facet capable client (#812)
Panels on iOS added OPDS facet and sort support in 3.13.0, so add it to
UserAgentNames.FACET_SUPPORT. Its CFNetwork style UA parses to "Panels"
through get_user_agent_name, so the existing exact-name match works.
Two more fixes came out of testing that allowlist.
Facet capable clients received the facets twice. The gate in the v1 feed
`entries` property had been commented out, so facets() ran unconditionally
and its OPDS1Link objects were appended to the entries list alongside the
real facet links from _links_facets. OPDS1TemplateEntrySerializer drops
every field those links don't have, so each one rendered as a dead entry:
empty id, no links, unclickable. Restore the gate so the fake navigation
folders are emitted only for clients that can't read facets.
The opds:facetGroup attribute carried internal query parameter names, and
clients like Panels show them verbatim as filter menu headings. Add a
display_name to the FacetGroup dataclass and emit that instead, so the
headings read "Order By", "Order Direction" and "Views". The query param
still drives hrefs and active-facet detection. facet_group also becomes a
CharField; it was typed as a collection-name ChoiceField whose choices
never included any value actually assigned to it.
Adds tests/test_opds_user_agent.py covering both facet variants, the
display names, and User-Agent parsing. Each test fails without its
corresponding fix.
Fixes #810
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* bump version to 2.2.9
* update deps
* memoize use facets order
* fix(opds): gate Panels facet support on build number (#815)
Panels' facet support is per platform and the platforms share one UA
name: the macOS build (951) does not render OPDS facets while iOS builds
(952 and later) do. Matching FACET_SUPPORT on the name alone sent macOS
Panels facet links it can't render, and with no fake nav folders either
it had no sort UI at all.
get_user_agent_name now also returns a build number, parsed from the
token right after the first slash for clients listed in _BUILD_UA_NAMES
(Panels only today). The auth mixin memoizes the pair and exposes it as
user_agent_name and user_agent_build, so existing name consumers are
unchanged. use_facets requires the client's build to meet
UserAgentNames.FACET_SUPPORT_MIN_BUILD (Panels: 952) in addition to name
membership; a missing or unparseable build fails the floor, falling back
to the fake nav folder sort that works on every client. Clients without
a floor, like kybooks, are unaffected.
No cache change needed: the response cache already varies on the full
User-Agent header, so builds 951 and 952 key separately.
Updates tests/test_opds_user_agent.py: the iOS constant moves to build
952, a new test pins that build 951 keeps the nav folder sort and gets
no facet links (it fails against the name-only gate), and the parse unit
tests cover the (name, build) pair including unparseable builds.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* Migrate deprecated EMAIL_* settings to Django's MAILERS (#816)
Django 6.1 deprecates the discrete EMAIL_* connection settings
(RemovedInDjango70Warning at django.setup()), and defining MAILERS makes
reading the old names an AttributeError.
- Replace the eight deprecated settings with an EMAIL_CONNECTION_OPTIONS
dict (TOML/env layer, keyed by EmailBackend constructor kwarg) plus a
MAILERS declaration pointing at the DB-aware DBEmailBackend.
- get_email_connection_kwargs / get_email_from_address coalesce the
EmailSettings DB row over EMAIL_CONNECTION_OPTIONS instead of the
removed settings.
- DBEmailBackend defaults its mailer alias so direct construction never
hits the SMTP parent's pre-MAILERS settings fallback.
- The admin test-send view builds the backend directly and calls
send_messages(), dropping deprecated get_connection() and
EmailMessage(connection=...).
- Tests override MAILERS + EMAIL_CONNECTION_OPTIONS instead of
EMAIL_BACKEND/EMAIL_HOST.
Claude-Session: https://claude.ai/code/session_01Bqa2ULBSaSVDwDSMEni1hf
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opds): denylist facet-blind Panels builds instead of a build floor (#817)
The build >= 952 floor was built on a wrong premise: Panels build
numbers interleave across platforms. Real iOS builds run lower than the
macOS build - 942 (reported in the field) and 950 (issue #810's
reporter) both render facets natively, while macOS 951 does not - so no
floor can separate them, and 952 shut real iOS users out of facets,
handing them the fake nav folder sort instead.
Replace FACET_SUPPORT_MIN_BUILD with FACET_BLIND_BUILDS, a per-client
frozenset of known facet-blind builds (Panels: {951}). use_facets now
refuses only those builds; every other build - unknown and unparseable
ones included - gets facets, which is the pre-gate behavior that worked
on iOS. A future facet-blind macOS build must be added to the set as
discovered; until then it receives facets it ignores, the pre-gate
status quo.
The iOS test constant moves to the field-reported build 942, an
unparseable-build UA joins the facet-capable cases to pin the
facets-by-default behavior, and the macOS 951 test still asserts the
nav folder fallback. The 942 and unparseable cases fail under the old
floor gate and pass under the denylist.
Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN
Co-authored-by: Claude <noreply@anthropic.com>
* update deps
* update deps
* update comicbox version with fix
* update deps
* update deps
* fix(tagging): follow CBR->CBZ conversion through rename and DB sync
Writing tags to a CBR converts it to CBZ (comicbox repacks unwritable
archives) and, with delete_original, removes the .cbr. The rename pass
then opened the dead .cbr path and failed with 'does not exist', and the
comic's row stayed pointed at the removed file. The converted CBZ is a
new inode, so neither the watcher's nor the poller's inode move
detection could ever pair old to new: the row was deleted and recreated
as a fresh comic, losing bookmarks.
Consume comicbox 4.8.5's WriteResult.final_path so the writer knows
where each archive ended up. The rename pass now chases the written
file to its post-conversion path, and a new conversion-aware DB sync
replaces the split rename/unwatched-reimport enqueues: a delete_original
conversion is recorded as a targeted move — for watched libraries too,
since the watcher cannot pair it; its later add/delete events reconcile
against the already-moved row. Conversions that keep the original leave
the row alone and report the new CBZ as a created file. Pure renames
and in-place writes keep their existing watcher-aware behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tagging): record watched-library renames instead of inferring them (#822)
* update deps
* fix(tagging): follow CBR->CBZ conversion through rename and DB sync
Writing tags to a CBR converts it to CBZ (comicbox repacks unwritable
archives) and, with delete_original, removes the .cbr. The rename pass
then opened the dead .cbr path and failed with 'does not exist', and the
comic's row stayed pointed at the removed file. The converted CBZ is a
new inode, so neither the watcher's nor the poller's inode move
detection could ever pair old to new: the row was deleted and recreated
as a fresh comic, losing bookmarks.
Consume comicbox 4.8.5's WriteResult.final_path so the writer knows
where each archive ended up. The rename pass now chases the written
file to its post-conversion path, and a new conversion-aware DB sync
replaces the split rename/unwatched-reimport enqueues: a delete_original
conversion is recorded as a targeted move — for watched libraries too,
since the watcher cannot pair it; its later add/delete events reconcile
against the already-moved row. Conversions that keep the original leave
the row alone and report the new CBZ as a created file. Pure renames
and in-place writes keep their existing watcher-aware behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tagging): record watched-library renames instead of inferring them
A rename in a watched library enqueued nothing, on the premise that the
watcher's inode pairing would detect the move on its own. That premise
does not hold for PDFs. pdffile's save() writes a temp file and
replace()s it over the original, so an "in-place" PDF tag write leaves a
new inode at the same path. The following rename then reaches the
watcher as an unpairable delete+add — detect_moves compares the row's
stale stored inode against the renamed file's fresh one — and the Comic
row is deleted and recreated, losing bookmarks and read state. Even a
same-inode archive goes unpaired when its delete and add land in
different watcher batches.
Enqueue the targeted move for watched libraries too. Codex performed the
rename; it should state the move rather than leave the watcher to
re-derive it from inodes. Scoping this to PDFs by file_type would freeze
a snapshot of comicbox's per-format write strategy from another repo:
its CBZ path patches the zip in place today, but is non-atomic, and an
obvious future move to tmp+replace would silently reintroduce this bug.
Duplicating a move the watcher does pair is safe: whichever copy lands
second is dropped by _remove_file_move_collisions for an occupied
destination, or matches no source row in _bulk_comics_move_prepare. As
with a conversion, a watcher delete that lands first degrades to the old
delete+recreate — never worse.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(tagging): hold a tag write's paths against a scan that lands mid-batch (#823)
A tag write that converts archives (CBR->CBZ with delete_original) records
the conversion as one targeted move ImportTask enqueued when the whole
batch finishes. A batch long enough to force a mid-batch watcher flush
(60s of continuous activity) or to catch a poll gets that scan's task
enqueued first, and ScribeThread's PriorityQueue breaks ties between equal
priority ImportTasks by enqueue time. The scan ran first, deleted the
comic rows by their now-dead paths -- cascading bookmarks away -- and left
the move with no source row. Short writes were safe; only long ones lost
data, which the conversion fix documented as best-effort.
Register every path a pending move passes through (the DB source, the
interim converted archive, the destination) in a process-local registry,
and drop registered paths from a task's created/modified/deleted sets in
init_apply, the importer's first phase. A task that carries the registered
move reconciles it, so it releases the guard and is exempt from it; the
exemption is computed from the task rather than from the release so an
unappliable move cannot cost a task its own paths. Ordering stops
mattering: the scan becomes a no-op for those paths whenever it runs, and
this covers the poller as well as the watcher.
Guarding creates matters as much as guarding deletes. Without a rename,
the move's destination is the interim CBZ, and a scan that imported it
first would leave the move to be dropped as a destination collision --
stranding the original row, bookmarks and all, on the dead path.
Port the poller's _is_move_compatible file-type and size check into the
watcher's inode move detection. A bulk conversion mass-frees CBR inodes
while mass-creating CBZ files, so on an inode-reusing filesystem a new CBZ
can be handed the inode a different comic's CBR just released, re-pathing
one comic's row onto another comic's file. The size check is waived when
the same batch also reports the source as written, which is the in-place
write-then-rename flow whose stored size is legitimately stale. Since
#822 codex states its own renames rather than leaving them to be paired,
so the check now only gates moves inferred for third-party changes.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(tagging): one status column per source in the admin status table
The online tagging status table showed a single Status and a single
Source per comic, which could not answer the question the admin actually
has during a two-source scan: which source is in which state. A comic
matched by Metron while Comic Vine sat out a rate limit read as one
"Matched" row with one source chip, and a source's rate-limit wait was
only visible in the strip above the table.
Replace both columns with one column per source the session selected, in
priority order, so each row reads across as that comic's per-source
state: Matched, Looking up, Waiting (rate limited), No match, Needs
review, User matched/skipped, or Error. A source the session did not
select gets no column at all; the columns are driven by batch.sources,
which the snapshot already carried and the frontend had never read.
comicbox already emits per-source events…
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.
Summary
Silences the boot-time
RemovedInDjango70Warnings (The EMAIL_HOST_PASSWORD setting is deprecated. Migrate to MAILERS before Django 7.0.and its seven siblings) by migrating off the discreteEMAIL_*connection settings onto Django 6.1'sMAILERSdict. The warnings pointed atcodex/__init__.py:16because that's thedjango.setup()call that loadscodex.settings; the deprecated settings were all codex's own — no third-party dependency defines or reads them.Once
MAILERSis defined, Django makes reading the old names anAttributeErrorand forbids mixing them, so the switch has to be atomic:codex/settings/__init__.py— the eight deprecated settings (EMAIL_HOST,EMAIL_PORT,EMAIL_HOST_USER,EMAIL_HOST_PASSWORD,EMAIL_USE_TLS,EMAIL_USE_SSL,EMAIL_TIMEOUT,EMAIL_BACKEND) become anEMAIL_CONNECTION_OPTIONSdict (same TOML/env sources, keyed byEmailBackendconstructor kwarg) plus aMAILERS = {"default": {"BACKEND": "codex.mail.DBEmailBackend"}}declaration.MAILERScarries noOPTIONSon purpose — baking values in would defeat the backend's per-send DB → settings resolution.DEFAULT_FROM_EMAIL,SERVER_EMAIL,EMAIL_SUBJECT_PREFIX, andEMAIL_ENABLEDare not deprecated and stay.codex/settings/db.py—get_email_connection_kwargs()/get_email_from_address()coalesce theEmailSettingsDB row overEMAIL_CONNECTION_OPTIONSinstead of the removed settings. Precedence (DB row → TOML/env → default) is unchanged.codex/mail.py—DBEmailBackenddefaults its maileraliasbefore delegating to the SMTP parent. Without an alias, Django's SMTP backend falls back to resolving params from the pre-MAILERSEMAIL_*settings, which now raiseAttributeError.codex/views/admin/email.py— the test-send view constructsDBEmailBackenddirectly and callssend_messages(), replacingget_connection()andEmailMessage(connection=...), which are both themselves deprecated in Django 6.1. Form-override → DB row → settings precedence is unchanged.tests/test_password_reset.py—@override_settingsnow overridesMAILERS(locmem backend) +EMAIL_CONNECTION_OPTIONSinstead ofEMAIL_BACKEND/EMAIL_HOST.Verification
Run against the
developbase:django.setup()emits zeroRemovedInDjango70Warning(verified with awarnings.catch_warnings(record=True)capture that also exercises the settings-fallback read paths and direct backend construction).settings.EMAIL_HOSTnow raisesAttributeError: The EMAIL_HOST setting is not available when MAILERS is defined.as Django intends.ruff check,ruff format --check,ty check, andbasedpyrightall pass on the changed files. One# ty: ignore[invalid-context-manager]was needed where django-types' stub declaresBaseEmailBackend.__exit__params non-optional (a stub bug; the runtime signature is fine, and basedpyright agrees).The one remaining
RemovedInDjango70Warningin the test suite is an unrelated, pre-existingselect_related()-with-no-arguments deprecation in the importer, left out of scope here.🤖 Generated with Claude Code
https://claude.ai/code/session_01Bqa2ULBSaSVDwDSMEni1hf