Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 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
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Django app served by Granian (ASGI). Key subsystems:
Bookmark, Identifier. SQLite with WAL mode.
- **`views/`** — DRF ViewSets organized by feature: `browser/` (comic listings),
`reader/` (page serving), `admin/` (CRUD), `opds/` (syndication).
- **`urls/`** — API at `/api/v3/`. Sub-routers: `/auth/`, `/c/` (reader),
- **`urls/`** — API at `/api/v4/`. Sub-routers: `/auth/`, `/c/` (reader),
`/<collection>/` (browser), `/admin/`.
- **`serializers/`** — DRF serializers for browser, reader, and admin responses.
- **`librarian/`** — Multiprocessing background daemon with dedicated threads:
Expand All @@ -66,7 +66,7 @@ Vue 3 + Vite + Vuetify 4 SPA.

- **`src/stores/`** — Pinia stores: `browser`, `reader`, `auth`, `metadata`,
`socket`, `admin`.
- **`src/api/v3/`** — HTTP client (xior) with automatic CSRF token injection.
- **`src/api/v4/`** — HTTP client (xior) with automatic CSRF token injection.
- **`src/components/`** — Organized by view: `browser/`, `reader/`, `admin/`,
`metadata/`, `settings/`.
- **`src/plugins/`** — Vue Router, Vuetify, drag-scroll.
Expand Down
13 changes: 13 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ width: 128px;
border-radius: 128px;
" />

## v2.2.4

- Features
- Metron Cloud online tagging uses an API key, generated on your
metron.cloud account page (comicbox 4.7.1). Saved usernames and passwords
keep working until you save a key, which replaces them.
- The custom URL fields for Metron Cloud and Comic Vine are removed. The
Metron one never did anything — mokkari hardcodes the metron.cloud
endpoint. Any saved URLs are discarded on upgrade.
- The interactive Swagger API docs are back, at `/api/v4/`. They went away
in v2.0.0 with the v3 API; only the raw OpenAPI schema at `/api/v4/schema`
remained. Admin login required, as before.

## v2.2.3

- Features
Expand Down
2 changes: 1 addition & 1 deletion codex/failed_login_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
separate file. The main stdout / codex.log sinks apply the inverse filter
(:func:`not_failed_login_filter`) so the IP-bearing line **only** lands in the
dedicated log — Django's own request logger still records the bare
``"Unauthorized: /api/v3/auth/login/"`` at WARNING so the failure is visible
``"Unauthorized: /api/v4/auth/login/"`` at WARNING so the failure is visible
in the main log, just without the client IP. Concentrating IPs in one place
makes the privacy story easier to reason about (one file to chmod, one file
to forward to a SIEM, one file to retain on a different schedule).
Expand Down
10 changes: 6 additions & 4 deletions codex/librarian/onlinetag/credential_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,18 @@ def _extract_rate_limits(status: RateLimitStatus) -> RateLimitInfo | None:


def _validate_metron(creds: OnlineCredentials) -> ValidationResult:
if not (creds.metron_user and creds.metron_password):
return ValidationResult(ok=False, error="Username and password required.")
if not (creds.metron_key or (creds.metron_user and creds.metron_password)):
return ValidationResult(ok=False, error="API key required.")
from mokkari.exceptions import ApiError, AuthenticationError
from mokkari.session import Session

# ``or None`` is load bearing: mokkari sends a Bearer header whenever
# api_token is not None, so an empty string would defeat the legacy
# username & password fallback.
session = Session(
username=creds.metron_user,
passwd=creds.metron_password,
api_token=creds.metron_key or None,
cache=None,
user_agent="codex-credential-check",
)
Expand Down Expand Up @@ -133,8 +137,6 @@ def _validate_comicvine(creds: OnlineCredentials) -> ValidationResult:
"cache_expiry": DO_NOT_CACHE,
"ratelimit_path": tmp_path / "ratelimits.sqlite",
}
if creds.comicvine_url:
kwargs["base_url"] = creds.comicvine_url
cv = Comicvine(**kwargs)
try:
cv.list_publishers(params={"limit": "1"}, max_results=1)
Expand Down
7 changes: 2 additions & 5 deletions codex/librarian/onlinetag/explicit_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,11 @@ def _build_auth_source(
"""Map codex credentials onto comicbox's per-source auth for one source."""
if source == "metron":
return OnlineSourceCredentials(
key=credentials.metron_key,
user=credentials.metron_user,
password=credentials.metron_password,
url=credentials.metron_url,
)
return OnlineSourceCredentials(
key=credentials.comicvine_key,
url=credentials.comicvine_url,
)
return OnlineSourceCredentials(key=credentials.comicvine_key)


def build_explicit_id_config(
Expand Down
14 changes: 10 additions & 4 deletions codex/librarian/onlinetag/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,27 @@ def _build_credentials(self) -> OnlineCredentials | None:
defaults = ComicboxTaggingDefaults.objects.get(pk=1)
except ComicboxTaggingDefaults.DoesNotExist:
return None
if not defaults.metron_user and not defaults.comicvine_key:
if (
not defaults.metron_key
and not defaults.metron_user
and not defaults.comicvine_key
):
return None
return OnlineCredentials(
metron_key=defaults.metron_key or "",
metron_user=defaults.metron_user or "",
metron_password=defaults.metron_password or "",
metron_url=defaults.metron_url or "",
comicvine_key=defaults.comicvine_key or "",
comicvine_url=defaults.comicvine_url or "",
)

@staticmethod
def _source_has_credentials(credentials: OnlineCredentials, source: str) -> bool:
"""Whether ``credentials`` actually carries auth for ``source``."""
if source == "metron":
return bool(credentials.metron_user and credentials.metron_password)
return bool(
credentials.metron_key
or (credentials.metron_user and credentials.metron_password)
)
if source == "comicvine":
return bool(credentials.comicvine_key)
return False
Expand Down
6 changes: 2 additions & 4 deletions codex/librarian/telemeter/admin_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def _default_sources(defaults: ComicboxTaggingDefaults) -> dict[str, int]:


def get_tagging_stats() -> dict[str, Any]:
"""Report the online tagging defaults. Never the credentials or urls."""
"""Report the online tagging defaults. Never the credentials."""
defaults = ComicboxTaggingDefaults.objects.first()
if not defaults:
return {}
Expand All @@ -157,11 +157,9 @@ def get_tagging_stats() -> dict[str, Any]:
"default_sources": _default_sources(defaults),
"default_format_count": len(formats) if isinstance(formats, list) else 0,
"has_metron_credentials": bool(
defaults.metron_user and defaults.metron_password
defaults.metron_key or (defaults.metron_user and defaults.metron_password)
),
"has_comicvine_credentials": bool(defaults.comicvine_key),
"metron_url_set": bool(defaults.metron_url),
"comicvine_url_set": bool(defaults.comicvine_url),
}


Expand Down
23 changes: 23 additions & 0 deletions codex/migrations/0050_comicboxtaggingdefaults_metron_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Generated by Django 6.0.6 on 2026-07-28 12:00."""

from django.db import migrations

import codex.models.fields


class Migration(migrations.Migration):
"""Add metron_key API token to ComicboxTaggingDefaults."""

dependencies = [
("codex", "0049_reprints"),
]

operations = [
migrations.AddField(
model_name="comicboxtaggingdefaults",
name="metron_key",
field=codex.models.fields.EncryptedCharField(
blank=True, default="", max_length=512
),
),
]
30 changes: 30 additions & 0 deletions codex/migrations/0051_remove_comicboxtaggingdefaults_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Generated by Django 6.0.7 on 2026-07-29 06:10."""

from django.db import migrations


class Migration(migrations.Migration):
"""
Remove the custom Metron & Comic Vine URL overrides.

The Metron override was always a no-op: mokkari hardcodes its API
endpoint, and comicbox's metron source warns that the url is ignored.
The Comic Vine override worked but serves no purpose for this app.
comicbox still defines the fields on its credentials dataclass; codex
just stops passing them.
"""

dependencies = [
("codex", "0050_comicboxtaggingdefaults_metron_key"),
]

operations = [
migrations.RemoveField(
model_name="comicboxtaggingdefaults",
name="comicvine_url",
),
migrations.RemoveField(
model_name="comicboxtaggingdefaults",
name="metron_url",
),
]
7 changes: 5 additions & 2 deletions codex/models/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,14 @@ class PromptsModeChoices(TextChoices):
# of enabled sources. Admin default; overridable per scan.
merge_all_sources = BooleanField(default=False)

# metron_user & metron_password are legacy. Metron authenticates with an
# API token now, and the admin UI only accepts one. Existing logins keep
# working (metron.cloud and comicbox still accept them) until an API key
# is saved, which clears them. mokkari prefers the token when both exist.
metron_key = EncryptedCharField()
metron_user = EncryptedCharField()
metron_password = EncryptedCharField()
metron_url = URLField(max_length=256, blank=True, default="")
comicvine_key = EncryptedCharField()
comicvine_url = URLField(max_length=256, blank=True, default="")

# Active session id + pending prompts used to live here. They are
# transient operational state — they only matter while a tagging
Expand Down
4 changes: 1 addition & 3 deletions codex/serializers/admin/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class StatsTaggingSerializer(Serializer):
"""
Online Tagging Defaults.

Credentials and service urls report only whether they are configured.
Credentials report only whether they are configured.
"""

default_match_mode = CharField(required=False, read_only=True)
Expand All @@ -183,8 +183,6 @@ class StatsTaggingSerializer(Serializer):
default_format_count = IntegerField(required=False, read_only=True)
has_metron_credentials = BooleanField(required=False, read_only=True)
has_comicvine_credentials = BooleanField(required=False, read_only=True)
metron_url_set = BooleanField(required=False, read_only=True)
comicvine_url_set = BooleanField(required=False, read_only=True)


class StatsAuthSerializer(Serializer):
Expand Down
57 changes: 31 additions & 26 deletions codex/serializers/admin/tagging.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Comicbox tagging serializers."""

from typing import override

from comicbox.formats.base.online import SOURCE_NAMES
from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import (
Expand Down Expand Up @@ -102,11 +104,8 @@ class TaggingValidateRequestSerializer(Serializer):
source = ChoiceField(
choices=tuple(sorted(KNOWN_SOURCES)), required=False, allow_blank=True
)
metron_user = CharField(required=False, allow_blank=True)
metron_password = CharField(required=False, allow_blank=True)
metron_url = CharField(required=False, allow_blank=True)
metron_key = CharField(required=False, allow_blank=True)
comicvine_key = CharField(required=False, allow_blank=True)
comicvine_url = CharField(required=False, allow_blank=True)


class TaggingRateLimitWindowSerializer(Serializer):
Expand Down Expand Up @@ -142,26 +141,19 @@ class TaggingValidateResponseSerializer(Serializer):
class ComicboxTaggingDefaultsSerializer(BaseModelSerializer):
"""Serializer for ComicboxTaggingDefaults singleton."""

metron_user = CharField(write_only=True, required=False, allow_blank=True)
metron_password = CharField(write_only=True, required=False, allow_blank=True)
metron_key = CharField(write_only=True, required=False, allow_blank=True)
comicvine_key = CharField(write_only=True, required=False, allow_blank=True)

metron_user_set = SerializerMethodField()
metron_password_set = SerializerMethodField()
metron_key_set = SerializerMethodField()
comicvine_key_set = SerializerMethodField()

has_metron_credentials = SerializerMethodField()
has_comicvine_credentials = SerializerMethodField()

@staticmethod
def get_metron_user_set(obj) -> bool:
"""Whether a Metron username has been configured."""
return bool(obj.metron_user)

@staticmethod
def get_metron_password_set(obj) -> bool:
"""Whether a Metron password has been configured."""
return bool(obj.metron_password)
def get_metron_key_set(obj) -> bool:
"""Whether a Metron API key has been configured."""
return bool(obj.metron_key)

@staticmethod
def get_comicvine_key_set(obj) -> bool:
Expand All @@ -170,8 +162,12 @@ def get_comicvine_key_set(obj) -> bool:

@staticmethod
def get_has_metron_credentials(obj) -> bool:
"""Whether both Metron username and password are set."""
return bool(obj.metron_user and obj.metron_password)
"""
Whether Metron can authenticate: an API key or a legacy login.

Mirrors comicbox's ``MetronOnlineSource.is_configured``.
"""
return bool(obj.metron_key or (obj.metron_user and obj.metron_password))

@staticmethod
def get_has_comicvine_credentials(obj) -> bool:
Expand All @@ -183,6 +179,20 @@ def validate_default_sources(value: list) -> list:
"""Require known source names; preserve the priority order."""
return _validate_ordered_sources(value)

@override
def update(self, instance, validated_data):
"""
Retire the legacy username & password whenever the API key is written.

The key is the only Metron credential the UI offers now, so saving one
(or clearing it) drops the old login rather than leaving a stale
fallback that mokkari would silently ignore anyway.
"""
if "metron_key" in validated_data:
validated_data["metron_user"] = ""
validated_data["metron_password"] = ""
return super().update(instance, validated_data)

class Meta(BaseModelSerializer.Meta):
"""Specify model and fields."""

Expand All @@ -195,20 +205,15 @@ class Meta(BaseModelSerializer.Meta):
"default_prompts_mode",
"default_sources",
"merge_all_sources",
"metron_user",
"metron_password",
"metron_url",
"metron_key",
"comicvine_key",
"comicvine_url",
"metron_user_set",
"metron_password_set",
"metron_key_set",
"comicvine_key_set",
"has_metron_credentials",
"has_comicvine_credentials",
)
read_only_fields = (
"metron_user_set",
"metron_password_set",
"metron_key_set",
"comicvine_key_set",
"has_metron_credentials",
"has_comicvine_credentials",
Expand Down
Loading