Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,11 @@ RUN mkdir -p /home/abc/.config/comicbox \

COPY --from=wheel-installer /opt/codex /opt/codex
ENV PATH="/opt/codex/bin:${PATH}"
# Fail the build, not the container start, on a broken venv.
RUN python -B -c "import django, comicbox, pymupdf, PIL.Image, cryptography, granian, rapidfuzz"
# Fail the build, not the container start, on a broken venv. comicbox's
# doctor proves unrar extracts a RAR member, pymupdf loads and every pin is
# satisfied; HOME keeps the config dir it creates out of the image.
RUN python -B -c "import django, cryptography, granian, rapidfuzz" \
&& HOME=/tmp comicbox doctor -q

VOLUME /comics
VOLUME /config
Expand Down
18 changes: 17 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,26 @@ border-radius: 128px;

## v2.5.3

- Fixes
- Features
- Online tagging gains an Ask match mode that prompts for every match
instead of writing any.
- Admin Doctor tab: archive tools, PDF support, image codecs, database,
libraries, watcher limits, config and package problems, each with its fix.
Problems are also logged at startup.
- Fixes
- A missing or broken unrar no longer hides CBR comics from scans or deletes
them from the library; they are listed as failed imports with the cause.
- A broken PDF library disables PDF reading instead of stopping the server.
- Metron logins by username and password tag again; a blank API key no
longer overrides them.
- Credential checks no longer compete with a running tagging job for
Metron's rate limit.
- Re-applying a chosen online match reuses the cached search.
- Failed imports no longer vanish from the admin list after other imports.
- Size searches accept kb, mb, gb and tb units and the >= and <= operators.
- Codex shuts down cleanly on Python 3.12 and 3.13.
- Performance
- Docker image is about 165 MB smaller.

## v2.5.2

Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ apk add bsd-compat-headers build-base jpeg-dev libffi-dev libwebp openssl-dev sq

##### Install unrar Runtime Dependency on non-debian Linux

Codex requires unrar to read CBR formatted comic archives. Unrar is often not
Codex requires [RARLAB's unrar](https://www.rarlab.com) to read CBR comic
archives; the bsdtar and 7-Zip fallbacks cannot extract them. Unrar is often not
packaged for Linux, but here are some instructions:
[How to install unrar in Linux](https://www.unixtutorial.org/how-to-install-unrar-in-linux/)

Expand Down Expand Up @@ -1106,6 +1107,28 @@ codex like:
LOGLEVEL=DEBUG codex
```

### 🩺 Doctor

The Admin panel's Doctor tab reports whether this install can read each comic
archive format, has the image codecs cover matching needs, whether comicbox's
config and package versions are in order, and whether the database, config
directory, library folders, filesystem watcher and stored credentials are
usable, with a fix for each problem. Codex logs the problems when it starts,
too.

The same report runs from the command line, which helps when Codex will not
start. In Codex's Python environment:

```sh
comicbox doctor
```

In Docker:

```sh
docker exec codex comicbox doctor
```

### Watching Filesystem Events with Docker

Codex tries to watch for filesystem events to instantly update your comic
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 4 additions & 6 deletions codex/choices/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from types import MappingProxyType

from comicbox.config.online import Effort
from comicbox.config.online import Effort, MatchMode
from comicbox.formats.base.online import SOURCE_NAMES
from django.db.models.enums import TextChoices

Expand Down Expand Up @@ -50,12 +50,10 @@ class AdminFlagChoices(TextChoices):

TAGGING_CHOICES = MappingProxyType(
{
# comicbox's match modes, ask through eager, derived so the two
# cannot drift. Ask prompts for every match instead of writing.
"matchMode": MappingProxyType(
{
"careful": "Careful",
"auto": "Auto",
"eager": "Eager",
}
{mode.value: mode.value.capitalize() for mode in MatchMode}
),
"promptsMode": MappingProxyType(
{
Expand Down
108 changes: 108 additions & 0 deletions codex/doctor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
The doctor: comicbox's health checks plus codex's own, for this install.

comicbox 5.3.0's ``run_checks`` reports whether this host can read each
archive format, whether Pillow has the codecs cover matching needs, whether
the user's comicbox config parses, and whether comicbox's package pins are
satisfied, with a one-line fix for anything that is not. Codex adds the
checks for what it needs on top (``codex.doctor.checks``), shows the rows
to admins under two headings, and logs the problems once at startup, so a
missing unrar or a full disk is explained in one place rather than
discovered as failed imports with no cause.

comicbox's Online section is left out. Codex keeps the tagging credentials
in its own database and checks them on the Tagging tab, so the doctor's "no
credentials" rows and its ``--online`` advice would describe the CLI, not
this server.

comicbox caches its tool probes for the life of the process, so a tool
installed while codex runs shows up after a restart.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from comicbox.doctor import CheckResult, Status, run_checks
from comicbox.doctor.online import SECTION as _ONLINE_SECTION

from codex.doctor.checks import CHECKS, SECTION

if TYPE_CHECKING:
from loguru import Logger

from codex.doctor.checks import Check


@dataclass(frozen=True, slots=True)
class DoctorReport:
"""comicbox's rows and codex's own, with the host they ran on."""

#: comicbox version, Python, platform: information only.
header: tuple[str, ...]
comicbox: tuple[CheckResult, ...]
codex: tuple[CheckResult, ...]

@property
def results(self) -> tuple[CheckResult, ...]:
"""Every row, comicbox's first."""
return (*self.comicbox, *self.codex)

@property
def problems(self) -> int:
"""How many rows say something codex needs is broken."""
return sum(row.status.is_failure for row in self.results)


def _run_check(name: str, check: Check) -> list[CheckResult]:
"""Run one codex check; a crash becomes one ERROR row and hides nothing else."""
rows: list[CheckResult] = []
try:
rows.extend(check())
except Exception as exc: # reported as the row
rows.append(
CheckResult(
SECTION, name, Status.ERROR, detail=f"{type(exc).__name__}: {exc}"
)
)
return rows


def run_doctor() -> DoctorReport:
"""Run comicbox's doctor and codex's checks; keep the rows that describe this server."""
report = run_checks()
comicbox_rows = tuple(
row for row in report.results if row.section != _ONLINE_SECTION
)
codex_rows = tuple(row for name, check in CHECKS for row in _run_check(name, check))
return DoctorReport(header=report.header, comicbox=comicbox_rows, codex=codex_rows)


def _describe(row: CheckResult) -> str:
"""One log line for a row: what, how bad, why, and the fix if there is one."""
line = f"doctor: {row.name} {row.status}: {row.detail}"
if row.fix:
line += f" Fix: {row.fix}"
return line


def log_doctor_problems(log: Logger) -> None:
"""
Log what the doctor found, once, after logging is configured.

Failures warn and warnings inform. The scanner used to warn about an
unreadable archive format at import time, before any log sink existed,
so the message only ever reached stderr.
"""
report = run_doctor()
for row in report.results:
if row.status.is_failure:
log.warning(_describe(row))
elif row.status is Status.WARN:
log.info(_describe(row))
if problems := report.problems:
noun = "problem" if problems == 1 else "problems"
log.warning(f"The doctor found {problems} {noun}. See the Admin Doctor tab.")
else:
log.debug("The doctor found no problems.")
Loading