Conversation
ajslater
commented
Aug 24, 2026
Owner
- Fixes
- Fix a rare crash opening RAR3 extended timestamps with CBR files.
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>
…ect.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>
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>
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>
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>
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>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
commit 9db6fe273622635700defb5c9015bf540630e40a Merge: c8984b9ed ed38cb5 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 cd84ed9 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>
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>
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>
An un-nested overlay is silently ignored by confuse.
…y 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): 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>
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>
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>
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>
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>
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>
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>
) 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>
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>
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>
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>
#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>
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.