Skip to content

v2.2.6 - #806

Merged
ajslater merged 79 commits into
mainfrom
develop
Aug 4, 2026
Merged

v2.2.6#806
ajslater merged 79 commits into
mainfrom
develop

Conversation

@ajslater

@ajslater ajslater commented Aug 4, 2026

Copy link
Copy Markdown
Owner
  • Features
    • The Comic Vine custom URL is back, for tagging against a Comic Vine proxy
      or mirror. URLs discarded by v2.2.4 must be entered again. Metron Cloud's
      stays gone; it never did anything.

ajslater and others added 30 commits July 3, 2026 16:19
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>
ajslater and others added 29 commits July 26, 2026 14:29
The interactive docs were lost in the v4 cutover (9b8314d, 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>
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>
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 8fbf7e1 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>
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>
…ogins

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>
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>
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2.2.4 (0dd3e51) 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>
@ajslater
ajslater merged commit 0d326cb into main Aug 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant