Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
063f2d2
fix(frontend): send passwordConfirm from profile change-password (#793)
ajslater Jul 3, 2026
3020d67
update deps and version to v2.1.1. bump news
ajslater Jul 3, 2026
27783ee
test(frontend): work around vitest/valid-expect false positive on exp…
ajslater Jul 4, 2026
4963ad9
update deps
ajslater Jul 4, 2026
2462a41
fix(onlinetag): drop dead effort option; model Metron's fewer requests
ajslater Jul 4, 2026
8245efa
trim news
ajslater Jul 4, 2026
6591384
Merge branch 'main' into develop
ajslater Jul 4, 2026
a1845af
refactor(onlinetag): derive source list + issue-id parser from comicbox
ajslater Jul 4, 2026
cd84ed9
fix(onlinetag): scope the match-mode request-count hint to Comic Vine
ajslater Jul 4, 2026
9980c04
fix(fs): don't let one unreadable folder crash the library scan
ajslater Jul 4, 2026
42b7fe9
update deps
ajslater Jul 4, 2026
dfbf390
update version to 2.1.2
ajslater Jul 4, 2026
1d06607
fix typechecking
ajslater Jul 4, 2026
07ffa2a
news for v2.1.2
ajslater Jul 4, 2026
ed38cb5
format
ajslater Jul 4, 2026
a41eede
Squashed commit of the following:
ajslater Jul 4, 2026
8ebc051
update deps
ajslater Jul 4, 2026
08eadba
refactor(onlinetag): extract resume-param sanitizer to cut complexity
ajslater Jul 4, 2026
227cea4
fix(onlinetag): reference defined constant in match-mode hint
ajslater Jul 4, 2026
76a7d3b
Merge branch 'main' into develop
ajslater Jul 4, 2026
97c62aa
update deps
ajslater Jul 7, 2026
f7abf7d
fix(settings): nest comicbox loglevel/delete_keys under general section
ajslater Jul 11, 2026
da257b0
fix(onlinetag): resolve prompts against current DB path, surface appl…
ajslater Jul 11, 2026
a5f37d8
update deps and format
ajslater Jul 11, 2026
e45ef90
Native OIDC single sign-on (Admin Auth tab) (#798)
ajslater Jul 13, 2026
d03666d
bump version 2.2.0
ajslater Jul 13, 2026
698164c
bump news
ajslater Jul 13, 2026
2de8473
update deps and comicbox
ajslater Jul 13, 2026
9243b5e
adapt ComicVine credential check to simyan v3 (comicbox 4.1.1)
ajslater Jul 13, 2026
4c97679
update deps
ajslater Jul 13, 2026
e6bb026
test(onlinetag): assert merge flag is forwarded, not comicbox's arith…
ajslater Jul 13, 2026
19f7b1b
Merge branch 'main' into develop
ajslater Jul 13, 2026
c7dc81f
update deps
ajslater Jul 19, 2026
cb98ea0
v2.2.1: show Metron account rate limits live (comicbox 4.3.0 / mokkar…
ajslater Jul 19, 2026
b3ec559
update deps
ajslater Jul 22, 2026
86b8403
v2.2.1: community ratings replace critical rating (comicbox 4.4.0)
ajslater Jul 22, 2026
04a7b2d
Merge branch 'main' into develop
ajslater Jul 22, 2026
4f7e516
update deps
ajslater Jul 24, 2026
f7ceb6c
v2.2.2: fix rotated pdf page serving (comicbox-pdffile 0.6.3)
ajslater Jul 24, 2026
8086c4c
update deps, including comicbox 4.5.0
ajslater Jul 24, 2026
00c3a99
Fix silent no-op when clearing tag editor fields
ajslater Jul 22, 2026
172a8c9
Fix lint errors from the ruff 0.16 upgrade
ajslater Jul 24, 2026
f1c8405
update comicbox
ajslater Jul 24, 2026
8be22e5
Clear monochrome by deleting the tag, not writing false
ajslater Jul 24, 2026
6b29b63
Merge branch 'main' into develop
ajslater Jul 24, 2026
1acc942
update devenv and deps
ajslater Jul 25, 2026
d358c22
update claude rules about telemetry
ajslater Jul 25, 2026
10d87b0
Fix anonymous stats sending and report what codex grew into
ajslater Jul 26, 2026
9b102cd
update deps
ajslater Jul 26, 2026
a92236d
Support comicbox 4.6.0 series alternative names
ajslater Jul 26, 2026
265f7fa
update deps and devenv
ajslater Jul 26, 2026
5d5b3bd
bump news for comicbox 4.6.1
ajslater Jul 26, 2026
3b500ae
update devenv
ajslater Jul 26, 2026
646fe38
fix cron double enqueue of telemeter task
ajslater Jul 27, 2026
c014acb
test crond double enqueu fix
ajslater Jul 27, 2026
7c65e0d
test telmemeter logging fixes
ajslater Jul 27, 2026
7574042
Merge branch 'main' into develop
ajslater Jul 27, 2026
4bec196
feat(api): reintroduce Swagger UI at /api/v4/
ajslater Jul 27, 2026
8650305
v2.2.4
ajslater Jul 27, 2026
1d0fc47
sub api v3 for v4
ajslater Jul 27, 2026
8fbf7e1
feat(tagging): authenticate to Metron with an API key
ajslater Jul 29, 2026
31d81d2
update deps
ajslater Jul 29, 2026
0dd3e51
feat(tagging): remove the custom URL fields for Metron & Comic Vine
ajslater Jul 29, 2026
0f412f8
fix(tests): clear the two outstanding ty errors
ajslater Jul 29, 2026
f740fff
Merge branch 'main' into develop
ajslater Jul 29, 2026
2a1a170
feat(tagging): present Metron as an API key source & warn on legacy l…
ajslater Jul 29, 2026
7a09ccd
update devenv
ajslater Jul 30, 2026
0c2a0a7
update deps
ajslater Aug 1, 2026
5732a22
fix(importer): import a renamed comic's new tags instead of failing
ajslater Aug 1, 2026
e39ce40
v2.2.5
ajslater Aug 1, 2026
25d1a6d
feat(tagging): combine online search and tag-by-id into one pass
ajslater Aug 2, 2026
97c65d6
update deps
ajslater Aug 2, 2026
f54e70b
style(news): prettier wrap the online tagging entry
ajslater Aug 2, 2026
178a05d
Merge branch 'main' into develop
ajslater Aug 2, 2026
f0b513f
feat(tagging): restore the custom URL for Comic Vine
ajslater Aug 4, 2026
25e4c7e
bump version and update deps
ajslater Aug 4, 2026
97a08ee
bump dockerfile source version
ajslater Aug 4, 2026
e5dec10
granian 2.8.0
ajslater Aug 4, 2026
7a6ee38
update deps
ajslater Aug 4, 2026
b82febd
update devenv
ajslater Aug 4, 2026
7aa1ad4
update deps
ajslater Aug 4, 2026
ba64c91
update devenv and deps
ajslater Aug 6, 2026
8717fcc
use nodejs26 in the builder
ajslater Aug 6, 2026
516b9cd
update deps
ajslater Aug 6, 2026
96f9aaa
remove old ty ignore
ajslater Aug 11, 2026
09d4072
update deps
ajslater Aug 11, 2026
2b90515
fix radon complexity warnings
ajslater Aug 11, 2026
e9454ac
update deps
ajslater Aug 14, 2026
39ed7cf
fix(importer): skip comic moves onto claimed paths
ajslater Aug 14, 2026
7724e8c
bump version
ajslater Aug 14, 2026
6e3ba89
update deps
ajslater Aug 14, 2026
4a2004b
Merge branch 'main' into develop
ajslater Aug 14, 2026
579d28e
update deps
ajslater Aug 16, 2026
446cab8
fix(update): make codex self-update work on macos and linux
ajslater Aug 16, 2026
b88554a
docs(proxy): document proxy_pass_header Server for nginx
ajslater Aug 16, 2026
03e54bd
update new for docs too
ajslater Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ width: 128px;
border-radius: 128px;
" />

## v2.2.8

- Fixes
- Codex updates itself again, automatically or on demand, and announces new
versions.
- Documentation
- Docs for passing through the Server: HTTP header so codex may be
identified by OPDS clients like Stump.

## v2.2.7

- Fixes
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,9 @@ location ^~ /codex {
# If the nginx credentials are different than codex credentials use this line to
# not forward the authorization.
proxy_set_header Authorization "";
# Let clients see "Server: codex/<version>" instead of "Server: nginx".
# nginx hides the upstream Server header by default. See below.
proxy_pass_header Server;
}
```

Expand All @@ -666,6 +669,37 @@ of the `/codex` location with `auth_request`, exempt the API the way Codex's own
examples do (`location /codex/api { auth_request off; ... }`); the `^~` note
above is what keeps the static assets reachable.

#### Preserving the Codex Server Header

Codex identifies itself with a `Server: codex/<version>` response header, which
is how OPDS clients, uptime monitors and support requests tell a Codex install
apart from whatever is proxying it. nginx does **not** pass that header through:
by default it hides `Date`, `Server`, `X-Accel-...` and `X-Pad` from the
upstream response and substitutes its own `Server: nginx/<version>`. Add
[`proxy_pass_header`](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass_header)
to the Codex location (as in the example above) to un-hide it:

```nginx
proxy_pass_header Server;
```

```console
$ curl -sI https://example.com/codex/ | grep -i '^server'
server: codex/2.2.8
```

Notes:

- `server_tokens off;` is not a substitute. It only shortens nginx's own header
to `Server: nginx`; the Codex header is still discarded.
- Like `proxy_set_header`, `proxy_pass_header` is inherited from the enclosing
`http {}` / `server {}` block **only if no `proxy_pass_header` is defined in
the location itself**. Keep it in the same block as the rest of the Codex
proxy headers.
- Other reverse proxies have their own rules for the `Server` header; if
`curl -sI` shows something other than `codex/<version>`, check your proxy's
documentation for passing upstream response headers through.

#### Nginx Reverse Proxy 502 when container refreshes

Nginx requires a special trick to refresh dns when linked Docker containers
Expand Down
4 changes: 3 additions & 1 deletion codex/librarian/bookmark/bookmarkd.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ def _process_task_immediately(self, task) -> None:
)
self._spawn_offline(
"latest-version",
lambda: worker.update_latest_version(force=task.force),
lambda: worker.update_latest_version(
force=task.force, update=task.update
),
)
case _:
self.log.warning(f"Unknown Bookmark task {task}")
Expand Down
98 changes: 73 additions & 25 deletions codex/librarian/bookmark/latest_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,96 @@

import json
from datetime import timedelta
from typing import Final
from threading import Lock
from time import monotonic
from typing import ClassVar, Final
from urllib.request import urlopen

from django.utils import timezone

from codex.choices.admin import AdminFlagChoices
from codex.librarian.scribe.janitor.status import JanitorCodexLatestVersionStatus
from codex.librarian.scribe.janitor.tasks import JanitorCodexUpdateTask
from codex.librarian.worker import WorkerStatusBase
from codex.models import Timestamp
from codex.models import AdminFlag, Timestamp
from codex.version import PACKAGE_NAME

_REPO_URL: Final = f"https://pypi.python.org/pypi/{PACKAGE_NAME}/json"
_CACHE_EXPIRY: Final = timedelta(days=1) - timedelta(minutes=5)
_REPO_TIMEOUT: Final = 5
_FAILURE_BACKOFF: Final = 600.0


class _FetchGate:
"""
Process wide guard against stampeding PyPI fetches.

Every ``CodexLatestVersionTask`` builds a fresh updater on its own
daemon thread, so this state cannot live on the instance. While the
cache is empty *every* request to ``/api/v4/version`` queues a fetch
task, so without the lock a slow or unreachable PyPI means one
5 second outbound connection per request. ``next_attempt`` adds a
cooldown after a failure so an outage doesn't retry on every hit.
"""

lock: ClassVar[Lock] = Lock()
next_attempt: ClassVar[float] = 0.0


class CodexLatestVersionUpdater(WorkerStatusBase):
"""Methods for fetching the latest version."""

@staticmethod
def _fetch_latest_version():
def _fetch_latest_version() -> str:
"""Fetch Latest Remotely."""
response = urlopen(_REPO_URL, timeout=_REPO_TIMEOUT)
source = response.read()
decoded_source = source.decode("utf-8")
return json.loads(decoded_source)["info"]["version"]
latest_version = json.loads(decoded_source)["info"]["version"]
if not latest_version:
reason = "Bad latest version fetched."
raise ValueError(reason)
return latest_version

def _is_fetch_due(self, ts, *, force: bool) -> bool:
"""Is the cached latest version stale enough to refetch."""
if not (
force or not ts.value or timezone.now() - ts.updated_at > _CACHE_EXPIRY
):
self.log.debug("Not fetching new latest version, not expired.")
return False
if not force and monotonic() < _FetchGate.next_attempt:
self.log.debug("Not fetching new latest version, waiting after a failure.")
return False
return True

def _fetch_latest_version_into_cache(self, ts, *, force: bool) -> None:
"""Fetch the latest version into the timestamp cache if it's due."""
if not self._is_fetch_due(ts, force=force):
return
if not _FetchGate.lock.acquire(blocking=False):
self.log.debug("Latest codex version fetch already in flight.")
return
try:
latest_version = self._fetch_latest_version()
except Exception:
_FetchGate.next_attempt = monotonic() + _FAILURE_BACKOFF
raise
finally:
_FetchGate.lock.release()
ts.value = latest_version
ts.save()
self.log.info(f"Saved new latest codex version {latest_version}.")

def _queue_update(self, ts) -> None:
"""Chain into an update task if the admin enabled automatic updates."""
if not ts.value:
return
flag = AdminFlag.objects.only("on").get(key=AdminFlagChoices.AUTO_UPDATE.value)
if not flag.on:
self.log.debug("Auto update is disabled, not updating codex.")
return
self.librarian_queue.put(JanitorCodexUpdateTask())

def update_latest_version(self, *, force: bool, update: bool = False) -> None:
"""Get the latest version from a remote repo using a cache."""
Expand All @@ -38,26 +102,10 @@ def update_latest_version(self, *, force: bool, update: bool = False) -> None:
try:
self.status_controller.start(status)
ts = Timestamp.objects.get(key=Timestamp.Choices.CODEX_VERSION.value)
do_fetch = (
force
or not ts.value
or (timezone.now() - ts.updated_at > _CACHE_EXPIRY)
)
if do_fetch:
latest_version = self._fetch_latest_version()
if not latest_version:
reason = "Bad latest version fetched."
raise ValueError(reason)
ts.value = latest_version
ts.save()
level = "INFO"
log_txt = f"Saved new latest codex version {latest_version}."
if update:
task = JanitorCodexUpdateTask()
self.librarian_queue.put(task)
else:
level = "DEBUG"
log_txt = "Not fetching new latest version, not expired."
self.log.log(level, log_txt)
self._fetch_latest_version_into_cache(ts, force=force)
if update:
# Only after a completed fetch, so the update decision
# sees the version this run just learned about.
self._queue_update(ts)
finally:
self.status_controller.finish(status)
11 changes: 10 additions & 1 deletion codex/librarian/bookmark/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ class ClearLibrarianStatusTask(BookmarkTask):

@dataclass
class CodexLatestVersionTask(BookmarkTask):
"""Get the latest version."""
"""
Get the latest version.

``update`` requests that a successful fetch chain into a
``JanitorCodexUpdateTask`` if the Auto Update admin flag is on.
Only the nightly janitor sets it. Chaining (rather than queueing
both tasks up front) keeps the update from racing ahead of the
fetch that tells it whether an update is even available.
"""

force: bool = False
update: bool = False
48 changes: 26 additions & 22 deletions codex/librarian/scribe/janitor/janitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,26 +86,30 @@
RemoveCoversStatus,
)

_NIGHTLY_TASK_CLASSES: Final[tuple[type[LibrarianTask], ...]] = (
CodexLatestVersionTask,
JanitorAdoptOrphanFoldersTask,
JanitorForeignKeyCheckTask,
JanitorFolderRelationsCheckTask,
JanitorIntegrityCheckTask,
JanitorFTSIntegrityCheckTask,
JanitorCleanFKsTask,
JanitorCleanCoversTask,
JanitorCleanupSessionsTask,
JanitorCleanupBookmarksTask,
JanitorCleanupSettingsTask,
JanitorCleanupFavoritesTask,
JanitorCleanupTaggingStateTask,
SearchIndexSyncTask,
SearchIndexOptimizeTask,
JanitorVacuumTask,
JanitorBackupTask,
JanitorDumpUserDataTask,
CoverRemoveOrphansTask,
_NIGHTLY_TASKS: Final[tuple[LibrarianTask, ...]] = (
# Instances, not classes: the version check carries arguments. It
# forces a fetch (the nightly run is the daily refresh) and asks the
# fetcher to chain into an update task, which it does only if the
# Auto Update admin flag is on.
CodexLatestVersionTask(force=True, update=True),
JanitorAdoptOrphanFoldersTask(),
JanitorForeignKeyCheckTask(),
JanitorFolderRelationsCheckTask(),
JanitorIntegrityCheckTask(),
JanitorFTSIntegrityCheckTask(),
JanitorCleanFKsTask(),
JanitorCleanCoversTask(),
JanitorCleanupSessionsTask(),
JanitorCleanupBookmarksTask(),
JanitorCleanupSettingsTask(),
JanitorCleanupFavoritesTask(),
JanitorCleanupTaggingStateTask(),
SearchIndexSyncTask(),
SearchIndexOptimizeTask(),
JanitorVacuumTask(),
JanitorBackupTask(),
JanitorDumpUserDataTask(),
CoverRemoveOrphansTask(),
)
_JANITOR_METHOD_MAP: Final[MappingProxyType[type, str]] = MappingProxyType(
{
Expand Down Expand Up @@ -147,8 +151,8 @@ def queue_nightly_tasks(self) -> None:
"""Queue all the janitor tasks."""
try:
self.status_controller.start_many(_JANITOR_STATII)
for task_class in _NIGHTLY_TASK_CLASSES:
self.librarian_queue.put(task_class())
for task in _NIGHTLY_TASKS:
self.librarian_queue.put(task)
Timestamp.touch(Timestamp.Choices.JANITOR)
except Exception:
self.log.exception(f"In {self.__class__.__name__}")
Expand Down
Loading