Conversation
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>
d88f4f1 split the "User Settings" table into "Settings by Session" and "Settings by User", but the spec kept its own copy of the section titles and still asserted the old name, so it failed. Rather than retype the new names into the test, the tab now declares its tables once as a SECTIONS list the template loops over, and exports the titles. The spec compares the rendered captions to that export, so a rename cannot leave a stale copy behind again. The assertion is the whole ordered caption list rather than a containment check per title. With the titles derived, containment would pass vacuously; comparing the list still catches a section that stops rendering, one rendered twice, or an empty title. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Unclip the settings button's librarian progress ring Vuetify 4.2.0 added `overflow: hidden` to `.v-btn` (fix(variant) #22992), which makes the button both the containing block and the clip box for the admin overlays inside it. The 32px progress ring does not fit the 28px compact icon button, so on xs it lost its left and bottom arcs; the safe-area padding clips it on notched phones in landscape too. Restore the pre-4.2 behavior for this one button. The scoped id selector is unlayered so it wins without `!important`, and nothing escapes that the Vuetify clip was meant to contain: `variant="plain"` hides the button overlay, the ripple container clips itself, and inside `v-toolbar-items` the button has no border radius. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep the filter menu's extra rows keyboard reachable Vuetify 4.2.0 moved select keyboard navigation into `useScrolling`, whose list keydown capture wraps from either end of the item rows and calls `stopImmediatePropagation()`. That pre-empts VList's own focus walk, which was what used to carry the user from the bookmark rows into the rows this menu contributes through the prepend/append slots, so "Clear All Filters", "Favorites Only" and the filter sub-menus became mouse-only. A capture listener on the overlay content runs before the list's, so stepping off either end of the bookmark rows now lands on the adjacent slot row instead of wrapping. Every other key and row is left alone and Vuetify still owns the navigation. The neighbour is found by walking the content's focusable rows rather than by naming the slot rows, so it follows the rows that are actually rendered: logged out, there is no "Favorites Only" and the step lands on the first filter sub-menu. Also cover the other 4.2 delta in this menu: `closeOnSelect()` now bails when `menuProps` carries `closeOnContentClick: false`, which leaves `onSubMenuSelected` as the only thing closing the menu after a pick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Drop the book change drawer's conditional width The drawer is 33vw rather than Vuetify's 256px default. Vuetify 4.1 parked an inactive layout item at `translateX(-(width prop + 1)px)`, computed from the prop and not the rendered box, so a 33vw drawer stayed partly on screen when closed. 24f8d1b worked around that by applying the width only while the drawer was open, which also made closing shrink it from 33vw to 256px mid-slide. Vuetify 4.2.0 parks it at `calc(+/-100% +/- 1px)` of its own rendered box, so the width can be unconditional and the close is a plain slide. Tests pin the offscreen transform that makes this safe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The vertical pager hands `v-virtual-scroll` a list of page numbers and reverses that list for bottom-to-top reading, so page 0 becomes the last item. `scrollToIndex` takes an index into that list, but `scrollToPage` passed the page number straight through, so every jump landed on the mirrored page: opening a comic at page 0 scrolled to the last page. Look the page up in `items` rather than repeating the reversal, so this keeps following however that list is built. Top to Bottom is unaffected, where a page number is already its own index; a test pins that too. Page tracking was never wrong. The intersection observer reads the real page number off the element, so only jumping to a page was affected. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(comicbox): adopt comicbox 5.0.0's moved and renamed APIs Installing comicbox 5.0.0 broke 27 test modules at import: MatchMode and its neighbours moved to comicbox.config.online, WriteMode became MergeMode, and ID_URL_KEY and NUMBER_TO_KEY are gone. Those cascade through the URL conf, so almost nothing ran. Identifiers no longer carry a url. Comicbox derives links from the key rather than storing a copy that could disagree with it, so codex derives its Identifier.url column the same way, through comicbox's own helpers. The bridge between the two type vocabularies -- codex names a type after the table it points at, comicbox after the thing itself -- is the IdentifierType enum's own member names, so neither side can drift. The online cache directory travelled as COMICBOX_ONLINE_CACHE_DIR, a name comicbox 5.0 no longer reads. It now rides a settings object handed to each session, which is not a name comicbox is free to change. The online settings deliberately carry no delete_keys: that set is the read side's parse-skip list, and online tagging writes what a database returns straight back to the archive. The fixtures become v3.0 documents. Two of their entries were malformed in a way 4.8.7 dropped silently and 5.0 now names in a warning: a null team voided the whole teams field, and a credit role's identifier was keyed by "key" instead of by its source. Both are repaired, which restores the coverage they were quietly costing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(identifiers): file an identifier under the type it states A comicbox 5 identifier names its type whenever that type isn't the one its position implies, so a comic can carry an id for its series or its volume among its own. Codex filed every comic-level identifier as an issue id and built the wrong url for the ones that weren't. Codex and comicbox name these types differently -- codex after the table the id points at, comicbox after the thing itself -- and the bridge is derived from the IdentifierType member names rather than written out a second time. A drift guard holds the two vocabularies together, and it found the first gap immediately: comicbox has a volume type codex had no member for. Migration 0054 adds it. The tag editor now speaks comicbox's names throughout. It used to offer codex's, glue the type onto the front of the key, and send an always empty url, while the add-by-URL endpoint beside it returned comicbox's names -- so a URL-added identifier landed on a type its own select could not display. A reprint keeps the name the file gave it, which is what stands in when comicbox parsed no series out of that name. MetronInfo's alternative names, the reason the old series sort_name fallback existed, no longer arrive as reprints at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(metadata): store the facts comicbox 5 separates out Comicbox 5 reports several things codex had no column for, and the importer's field whitelist is derived from comicbox's own schema, so every one of them was being skipped in silence. manga and reading direction are now two facts rather than ComicInfo's one compound YesAndRightToLeft value, and each gets a column and an editor field. MetronInfo's MangaVolume gets one too. A comic's web links used to ride along inside its identifiers. An identifier is now only a key -- codex derives its link from that key rather than storing a copy that could disagree -- so the links the file itself wrote down needed somewhere else to live. The primary credit flag moved onto each role, which is the point of it: the primary writer is not thereby also the primary inker. Codex stores it on the person-and-role pairing, and since credit rows are shared between comics, the flag has to be part of what identifies one. The series' other names are no longer reprints upstream, but they still mean what a reprint means -- another edition of this book -- so they keep sharing the table, flagged. The flag is what the editor consults to put each row back in the list it came from, and it splits the metadata panel's one row into the two things a reader is actually looking at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(onlinetag): spend API budget by effort, and price the run by it Comicbox 5 bounds Comic Vine's per-candidate fan-out by default and takes an effort setting to widen or narrow it, which codex had no way to reach: effort is not an OnlineSession keyword, it lives in the settings tree the session layers over. Codex now hands those settings across, so the same channel carries the cache directory and the scan's effort. The run estimate moves onto that axis too. It was keyed on match mode, which changes no request count at all -- match mode decides how a verdict is applied once the calls are already spent. The default projection roughly triples as a result. That is the estimate becoming honest about a cold search, not the run getting slower: a real scan batches by series, so one search answers for a whole series and beats the projection. Two identifier workarounds come out, now that comicbox files both cases correctly. A series' other names state that their ids name a series, so codex stops injecting that. A reprint's id is the reprinted issue's id, so it resolves to an issue page instead of to nothing. The sources strip learns that Comic Vine meters per endpoint pool rather than per account. It shows the pool with the least left, since that is the one that will stop the run, and says which window the number covers. A prompt is replayed by handing its fingerprint back to comicbox, and comicbox 5 changed what goes into one, so prompts stored by comicbox 4 can never match again. They are dropped at daemon start and nightly rather than by a migration: the tagging cache lives under the config directory, where a test run's migrations would reach real prompts. Comic Vine credential validation starts working. Simyan 3 returned a rejected key's error body verbatim, so the empty result read as success; simyan 4 raises, which the existing handler already caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(migrations): ask for one full re-read under comicbox 5 Comicbox 5 changes what existing files mean without changing the files: age ratings collapse to MetronInfo's seven names, credit roles canonicalize, ComicInfo's AlternateSeries reads as a story arc, a manga tag now also sets a reading direction, and a stated title stops being rebuilt from the story list. Codex re-reads a comic only when its file changed, so left alone a library would hold both readings at once -- the old one everywhere, the new one on whatever happened to be re-tagged -- and its browser facets would list Everyone beside Everyone 10+ as though they were different things. Clearing the stored stat is how the re-read is asked for: the poller reads a missing stat as "stat the file and call it modified", and the importer's prefilter skips only on a stat that matches or an unadvanced metadata mtime. It is resumable by construction, since a comic gets its stat back only once it has been imported. The import marker is deliberately left alone. It drives the "not imported" hint, and clearing it would blank that hint library-wide for the length of the re-read, which reads as data loss rather than as work in progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(news): record the comicbox 5 work under the unreleased 2.3.0 Folded into 2.3.0 rather than opening a 2.4.0: 2.3.0 has never been tagged, so there is no release anyone has seen for this to come after. The Dev section carries the two things an upgrading admin has to know before they see them happen -- every comic is read again on the first scan, and saved tagging prompts are discarded -- along with what the re-read will visibly change about their tags. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * add script * refactor(migrations): one migration, and no forced re-read Squashes the three comicbox 5 migrations into 0054, and drops the one that asked for a full re-read. Re-reading a library is an admin's call, not an upgrade's. The new columns can only be filled from the files, so filling them means opening every archive, and doing that unprompted on the first scan after an upgrade spends hours of someone's disk on a decision they were never asked about. Force Update Tags already does it, on whatever scope the admin picks, and the release notes now say so. The cost of not re-reading is a library that reads its tags two ways at once: age ratings and credit roles keep comicbox 4's spellings until a comic is next read, so a filter can offer both Everyone 10+ and Everyone. That is untidy rather than harmful -- age-restricted access derives from the Metron scale, which both spellings still map onto identically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(importer): key a credit that names no role A file may credit someone without saying what they did, and comicbox passes that through as a person with an empty role list. Codex built a key with only the person in it, and a short key is padded with nulls on its way to the database. That was survivable while the padded column was the role, which is nullable. The primary flag is not, so the first role-less credit in a library aborted the whole import with an IntegrityError -- taking every other comic in the batch with it. Such a credit now keys as (person, no role, not primary), which is what it means: there is no role for the person to be the primary holder of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * update deps * refactor(migrations): fold the comicbox 5 columns into 0053 0053 is unreleased too -- v2.2.11 ships 0052 -- so everything here lands in v2.3.0 together and a database has no reason to walk through it in two steps. Renamed to cover what it now does. This is the same fold as "Combine the two unreleased migrations into 0053", one release later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(telemeter): count the comicbox 5 tags, and show them honestly Comicbox 5 gave codex several new facts about a library and nothing counted any of them. The stats report now says how many comics are manga, carry a manga volume or carry web links, how many credits mark a primary holder, and how many reprints are a series' other names. It also reports the Effort default, which decides how much API budget a scan spends per comic. Counts only, and every key is a name fixed in this source. manga is three scalar counts rather than a bucket for exactly that reason: the payload's shape then cannot be invented by a value read out of a comic file, and a new comicbox manga value fails the serializer-registration test instead of quietly becoming a new key. Anything outside the vocabulary is folded into unknown, so the three still partition the library. Never the volume string, never the links, never a person or a role. Two of the columns needed their own counter. The existing one asks whether a column is NULL, and manga_volume and urls are NOT NULL with empty defaults, so it would have reported the whole library for both. There is a regression test for that, and the privacy suite now seeds a comic carrying both columns -- without one, the sentinel walk could not tell a count of them from a report of them. None of these can be backfilled, since they can only be filled by reading a file, so they all read zero until an install re-reads. That is in the release notes rather than left to be discovered. Then the Admin Stats tab, which is the visible contract for what leaves an install, and which had been misreporting it. The whole Settings by User table was empty: it read stats.per_user while the API renders the payload camelCased and ships perUser, so the only telemeter addition since 2.2.11 was invisible on the one page that exists to disclose it. Order By read "sortName" and Fit To read "W", because the labels were looked up with a case conversion applied to keys the choices files already emit camelCased. And four rows that detail the row above them rendered flat, because the indent set held plural spellings that matched no payload key. Boolean buckets read the wire's "true" and "false"; they now say On and Off, the same words chronicle's dashboard uses for those numbers. The tab's test fixture is now camelCased like the wire it stands in for, which is what let all three of those pass, and a new assertion requires every populated section to render at least one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * trim news * refactor(onlinetag): follow comicbox 5's OnlineSession rename comicbox 5 renames the session facade's mode= to match= and unattended= to prompts=, so the public API says what the CLI and the config keys have always said (ajslater/comicbox#205). Codex only ever passed mode=, so that is all that moves here, at both construction sites, plus the DeferredPrompt attribute the prompt serializer reads. The serialized prompt's own "mode" key stays: it is codex's cache format, versioned by PROMPT_VERSION, and renaming it would strand every prompt already persisted. Nothing else in the rename reaches codex. It never used unattended=, never called set_mode/set_unattended, and the only PromptResponse it builds is a plain "skip" — so the set_unattended -> set_prompts action rename, the one break that fails quietly rather than loudly, has no call site here. FakeDP carries .match now. The fake standing in for a comicbox object has to track the real attribute name or it hides exactly this kind of break. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * update deps * fix(covers): serve conditional GETs instead of caching response bodies The per-pk cover routes carried `cache_page`, which stored a copy of every cover response in the default cache. Because the page-cache key hashes the Vary header values, and these routes vary on Cookie (plus Authorization for OPDS), that meant one stored copy per user of bytes that already exist once on disk as a webp. Session-cookie rotation orphaned entries, a 100-cover page could populate 200+ of them, and a single pk could never be invalidated -- only a clear() of the whole cache, which import finish, tag writes, and Library/Group CRUD all do. Covers were collateral on every scan. Serve ETag and Last-Modified derived from the thumb's size and mtime instead. A revalidating client gets a 304 answered from the stat alone, without the image bytes being read, and a regenerated cover invalidates itself. Also: - /api/v4/covers/<source>/<pk>, the route the web client uses for every cover on a browse page, had neither Cache-Control nor Vary. It now has both; the ?ts=<mtime> cache-buster already made it safe to cache. - Comic covers are private rather than public. They are ACL-gated, so a shared proxy must not store them. Custom covers have no ACL and stay public. - Cover and reader-page views are exempt from throttling. `cache_page` used to short-circuit before DRF dispatched, so cover hits never reached the throttles; without it a browse page would spend 100 requests against a configured throttle.user rate for one user action. - BROWSER_TIMEOUT and READER_TIMEOUT are removed. No browser or reader route has ever been cache_page'd, so the MAX_ENTRIES comment was partly justifying a cache population that does not exist. Adds tests/test_cover_views.py, which had no predecessor -- the cover endpoints were previously untested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(onlinetag): let comicbox spare a large scan, and add a way back up Two things, found checking codex against the comicbox 5.0.0 release. Effort now defaults to Auto rather than Balanced. Comicbox reads any named effort as a decision to leave alone, and only while none is named is it free to drop a large unattended scan to minimal -- the protection it added precisely because balanced fans out enough Comic Vine calls to blow past the hourly cap and stretch a big library into hours. Codex named balanced on every scan, so nobody ever got that. Naming one is still available and still honored; it just isn't the default. The release also renamed the session's mode= to match= and unattended= to prompts=. Production code already followed that; one test fake had not. Separately: a group whose parents have gone missing yields a single breadcrumb, which is the current view and so no link at all, leaving a deep route with nothing to climb and no way out but the back button. It now offers the top, which is the one destination always there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(reader): close a book to a route that exists The close button's last-resort fallback read a key the browser defaults do not have, so the one branch meant to guarantee the reader stays closable would have thrown a TypeError and taken the render down with it. The defaults call that route lastRoute, which is what the router itself already falls back to. Unreachable today: the server always injects a last route, its own fallback being the admin default. It was a landmine rather than a live crash, and nothing exercised it, so a test now covers both the ordinary close and the fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Stop leaking a DB connection per ASGI request, and pool the workers that hold them (#838) * fix(db): stop leaking a database connection per ASGI request Every DB-touching request orphaned an open SQLite connection until the garbage collector emitted "ResourceWarning: unclosed database". A page of covers leaked dozens, three file descriptors apiece under WAL, which is what codex/run.py's RLIMIT_NOFILE bump was papering over. Django's ASGIHandler wraps each request in an asgiref ThreadSensitiveContext, which is backed by a private one-thread executor that is shut down when the request ends. django.db.connections is thread-local, so CONN_MAX_AGE=600 kept a connection alive on a thread about to be retired: never reusable, never closed. Django documents this ("When using ASGI, persistent connections should be disabled") and closed #33497 as documentation only, so this is by design, not a bug. Take the documented configuration: DATABASES leaves CONN_MAX_AGE at Django's default of 0, and the processes whose threads actually persist opt back in for themselves. The librarian does so as the first statement of its run loop, before it spawns the workers that reuse a connection across tasks. That covers every path Django controls, but not one it doesn't: send_response runs inside handle()'s TaskGroup, and if it raises, the exception group is re-raised before either request_finished or response.close(). A streaming body is consumed there, outside the middleware chain, so an iterator that raises is never converted to a 500 -- and codex streams comic archives off a user's library, where the librarian can move a file mid-download. ConnectionClosingApplication stays as the backstop for that exit, now documented as such. Two more connections nothing was closing: the admin restore endpoint's sidecar sqlite handle, and the admin stats view's off-loop query, which runs on an event-loop executor thread no request lifecycle cleans up. Also corrects six comments estimating the reconnect cost at "~5-20 ms". It measures ~1 ms, of which the PRAGMAs everyone worried about are ~0.01 ms; ~90% is SQLite re-parsing the schema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(db): serve requests on pooled workers that keep a warm connection Django gives every ASGI request a private worker thread and retires it at the end of the request, so each DB-touching request opened a fresh SQLite connection. Measured on this codebase that is ~0.76 ms, of which ~90% is SQLite re-parsing the schema (84 tables + 266 indexes); with per-connection statement re-preparation a five-query request pays ~1.26 ms, 53% of its database time. It also means cache_size=64MB and the 256MB mmap window can never warm on the HTTP path, and that a cold cover grid opens ~100 connections at three file descriptors each. Django's answer to this is pooling, but its pool is PostgreSQL-only and a SQLite equivalent is unsafe: a handle returned with an unexhausted cursor pins a WAL read snapshot that rollback() does not release, so the next borrower reads stale rows and its first write fails "database is locked". Pool the threads instead. WorkerPool owns a fixed set of ThreadSensitiveContext objects that are never entered, pre-seeds each with its own one-thread executor, and lends one to each request. asgiref's context manager is re-entrant, so Django's inner "async with" is a no-op and the worker -- with its thread-local connection -- outlives the request. That restores the stable-thread model CONN_MAX_AGE was written for, so the pool opts this process into persistent connections and Django's own request_started / request_finished hygiene recycles them exactly as under WSGI. Nine requests that opened six connections now open one. The pool reaches into two of asgiref's class attributes, which have been stable since 3.3.2 but are not a documented contract. So it checks them at startup rather than trusting them: if thread-sensitive work no longer lands on the seeded worker, the pool logs and disables itself, and every request takes the per-request path from the previous commit. asgiref is now a direct dependency so a major bump is a conscious upgrade, and a test pins the behavior so it fails loudly rather than silently degrading. Requests that would hold a worker for a client-speed transfer bypass the pool: the file downloads, and the health check, whose whole job is to answer even when the pool is saturated. Size it with db_workers under [server]; 0 restores per-request connections. Each worker can grow its own page cache, so lower it on a memory-constrained host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
typecheck (basedpyright), 3 warnings -> 0: - workers.py, librariand.py: build log messages before the call, since ruff's ISC003 rejects the explicit `+` form. - test_asgi_db_connections.py: mark SlowOrmApplication._query @OverRide. ty, 6 warnings -> 0. All were dead conditions; one hid a real bug: - search/sync.py: _update_search_index_clean was typed `-> None` and discarded the count from remove_stale_records, so `elif cleaned_count:` never fired and the search index summary never reported cleaned entries. Return the count (0 on the rebuild path, reported separately). - crond.py: `not self._task_times[0]` on a 2-tuple. - search/parse.py: `if match else` on a finditer result. - opds metadata/publications/manifest: `not OPDS_M2M_MODELS` guards against a 7-element module constant. complexity (radon cc), 3 C-rank functions -> A/B: - _build_browser_defaults C(11) -> B(6): extract _rename_table_columns, fold the repeated `row[x] or ""` / `bool(row[x])` lines into column tuples, and reuse _rename_sort_key for order_by. - test_new_sections_have_content -> A(5): assert list becomes a table. - test_facet_ua_gets_facet_links_no_fake_entries -> A(3): body extracted to _assert_facet_feed, three-way title check becomes one regex. Both restructured tests were mutation-checked to confirm they still fail when their invariant breaks. make fix/lint clean, 1094 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tatus, live status rail (#840) * Default Auto Update on outside Docker Installing a new codex over the running one is what a pip, pipx or uv install wants, so Auto Update now seeds on there. A container is an immutable deployment — an in-place upgrade only lives until the next restart brings the image's version back — so docker keeps seeding it off and updates by image pull. Seeding is still get_or_create, so an existing install (and an admin who turned the flag off) is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Report a source that ran and found nothing as No match Comicbox emits a terminal event only for the resolutions its matcher reaches. A search that comes back with zero candidates — routine for Metron, whose database is much smaller than Comic Vine's — ends that source's turn with a log line and no event at all, as does a search that raises and a comic with nothing to search on. The status table had no cell to render and fell back to an em-dash, which reads as "this source was never consulted": the one thing that had not happened. FileFinished now closes the comic. Every source that announced itself with SourceStarted (emitted after comicbox's first-wins skip, so a source that sat out never appears) and reported nothing settles as no_match, and an empty cell again means only what it looks like. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Move the status rail in step with the online lookup The admin drawer's rail renders the LibrarianStatus row, which an online tagging scan only touched when a comic *completed* — a minute of network per comic during which the rail sat still while the Tagging tab's table, fed by its own snapshot notification, plainly showed work happening. The live-lookup marker now names the source it is consulting in the status subtitle, so the rail gets the same heartbeat, paced by the live publish's existing one-second floor. Two supporting fixes in StatusController: - update() takes force, for a state change rather than a progress tick. Dropping one of those into the five-second coalescing window loses it instead of deferring it, since it *is* the whole update. The rate-limit path was already doing this by reaching in and backdating the status's own clock; it asks now. - The subtitle is written on every update, including an empty one. It was skipped as "nothing to write", so a phase's subtitle outlived it: a recovered rate limit kept describing the task as rate limited, and the importer's own subtitle clear never took. Also drops a verbatim duplicate of tagWriteErrorsNotified in the socket store. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Both of the previous PR's fixes hooked the event-emitting search session. Neither one fires for a re-tag, because comics codex already holds an issue id for never reach that session: the stored-id prepass fetches them by explicit id outside it, and on a re-tag that prepass is the whole run. Two consequences, both visible in the admin Tagging tab: - A merged multi-source refresh was credited to the primary source alone. Under merge-all the prepass pins every stored id and comicbox fetches and merges them all, so a comic refreshed from Metron *and* Comic Vine showed Matched in one column and an em-dash in the other — the cell that reads as "this source was never consulted". Each source whose requested id comes back in the merged record is now credited; the primary is proven by the fetch itself, which returns nothing unless its own id landed. A source the scan never pinned (first-wins, or no credentials) still reports nothing, which is what the em-dash is for. - Only the search pass opened a librarian status row, so a scan that resolved every comic from a stored id ran start to finish with an empty status rail, while the Tagging tab's table — published from the same loop — showed each lookup as it happened. The prepass now opens the row, totalled over the whole batch, advances it per comic, and the search pass adopts it instead of starting a second one, so the rail shows one continuous job whose elapsed time counts the prepass. The live marker's "looking up on <source>" subtitle lands there too, which is what it was missing to reach the rail at all. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
mkdocs refuses a site_dir inside docs_dir, so `make docs` has aborted with a configuration error since the target was written. Drop `--site-dir docs/site` and use mkdocs' default `site/` at the repo root, which every Python tool in pyproject.toml already excludes. Ignore `site/` in git, the Docker build context, and ESLint so a local docs build can't leak into commits, images, or `make lint`. readthedocs is unaffected: it passes its own --site-dir. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…to ghcr.io (#844) * Republish a deprecated Docker Hub mirror and warn its admins to move to ghcr.io Most installs reporting to chronicle are stuck on the 1.9.24 Docker Hub image: the Hub repo has no tags, and 1.9.24 never shows an outdated notice. Give those installs a pull that works and a loud reason to switch. - hub.Dockerfile: FROM the ghcr.io image plus one ENV and a deprecated label. Same layers, only the config blob differs; no RUN steps so no QEMU. - ci.yml deploy-hub job: builds and pushes docker.io/ajslater/codex:<v> and :latest for final versions, gated on DOCKERHUB_TOKEN so forks skip it. - DOCKER_IMAGE_DEPRECATED is now a boolean exposed as docker_hub in /api/v4/version and the session payload (the free-text warning is gone), and as platform.docker_hub in telemetry. - Admin-only top snackbar telling admins to change the image, dismissible for a day per browser, plus a permanent link in the settings footer. ghcr.io and native installs render nothing. - docs/DOCKER.md gains a "Migrating from Docker Hub" section the UI links to. - bin/lint-docker.sh and bin/fix-docker.sh now lint every top-level Dockerfile instead of only the first one found. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Use the existing DOCKER_USER / DOCKER_PASS secrets for the Docker Hub push The repo already holds these two secrets from the v1.10 registry switch and bin/docker-tag-latest.sh logs in with the same names, so the deploy-hub job should not ask for a second pair. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Every snackbar rendered on the app background instead of its own color. ``.v-snackbar__wrapper`` is a ``.v-overlay__content``, and app.vue painted all of those the theme background with ``!important`` from outside any cascade layer. Vuetify's ``bg-error`` / ``bg-warning`` classes live inside ``vuetify-utilities.theme-background``, so they never stood a chance: an unlayered declaration outranks a layered one whatever the specificity. Split the rule. The radius still applies to every overlay; the background now skips snackbar wrappers, so ``color`` reaches the element it is meant to paint. Menus, dialogs and tooltips keep the background they had. Give VSnackbar a default of ``color: "background"`` as well. Without it an uncolored snackbar would fall back to Vuetify's Material inverse surface, a light bar in this dark theme, and to unreadable dark-on-dark text. The default keeps today's look and explicit colors still win. The docker-hub snackbar no longer needs its local two-class override, so drop that and the marker class it existed for. Verified in the dev server: session-error paints rgb(220,20,60), filter-warning and docker-hub rgb(230,189,13), an uncolored snackbar rgb(18,18,18); menu, dialog and tooltip content boxes are unchanged at rgb(18,18,18) with the 5px radius and the tooltip's dimmed text. The production bundle keeps the layer order and leaves the app rules unlayered. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…mage (#846) * Warn the logs, not just the web UI, about the deprecated Docker Hub image An admin who runs codex headless and never opens the settings drawer had no way to learn that their image had stopped being the real one. Say it where they are actually looking. The warning names the registry to move to and links to the migration docs, and only the Docker Hub image ever emits it. It fires at startup beside the other lines about how this install is configured, and once a day from the janitor's version check rather than from a new job of its own. That call sits inside the fetch gate: while the version cache is empty every API hit queues a check, and only the one that wins the lock should nag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * update deps * more explicit simyan version --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
basedpyright no longer reports decorated pytest fixtures as unused, so the two reportUnusedFunction suppressions on the autouse fixtures were themselves flagged as unnecessary. Drop them. Split OnlineTagOutcomeStats._record_source_status (radon C, 11) along the seam it already had: the two lifecycle events that bracket a source's turn stay put, and the terminal verdicts move to _record_source_verdict, which now names the status once per event kind and makes the single _set_source_status call at the end. The SourceStarted body becomes _add_started_source, mirroring the existing _add_matched_source. The event classes are disjoint siblings, so case order carries no meaning and the dispatch is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cron loop asked for a timeout truncated to whole seconds, so every wait ended up to a second before the slot it waited for. It queued the job anyway, then recomputed a schedule off the same short clock, which handed back the slot just run: a timeout of zero, a wait that returns at once, and the whole nightly fan-out queued again on every pass until the clock caught up. Roughly thirty to fifty runs of the search-index optimize, vacuum, database optimize, backup and user-data snapshot every night. The weekly telemeter send shared the window. Wait out the real remainder instead, and run a job only once the clock has reached its slot. An early wake now costs one more trip around the loop rather than a night's work. Also compute the nightly slot as tomorrow's calendar date rather than now plus twenty four hours. On the night daylight saving time ends the local day is 25 hours long, so the hour after midnight resolved to the midnight that had just gone by, which would have spun for that whole hour. Co-authored-by: Claude Opus 5 <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.
once.