Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
4d950d6
feat: add plexscan (wedged scan recovery) + repair (dead-file re-grab…
Neoo-Blue Jun 14, 2026
b165306
feat: missing_seasons, no-upgrade profile, Plex empty trash + colored…
machetie Jun 16, 2026
6e4bb48
fix: preserve exc_info/stack_info in _ColorFormatter by delegating to…
machetie Jun 16, 2026
d8ab000
fix: address five code review issues
machetie Jun 16, 2026
01f65a8
feat: integrate westrepair (repair.py + shared libs) into single cont…
machetie Jun 18, 2026
ed2f7d9
feat: merge upstream plexscan + repair checks (PR #1)
machetie Jun 18, 2026
f67be98
feat: merge main into plexscan-repair branch (full feature set)
machetie Jun 18, 2026
5764ac9
refactor: remove westrepair subprocess in favor of native repair check
machetie Jun 18, 2026
5a46f39
feat: port best-of-westrepair improvements into native repair check
machetie Jun 18, 2026
bc7a406
refactor: replace missing_seasons subprocess with native check_missin…
machetie Jun 18, 2026
622ef50
fix: extend janitor to catch debrid-specific dead-link log patterns
machetie Jun 18, 2026
b417bce
fix: repair sonarr re-search now tries SeasonSearch before EpisodeSearch
machetie Jun 18, 2026
e0a6754
feat: add MissingFromDisk history-based repair mode for usenet/direct…
machetie Jun 18, 2026
8362149
docs: document repair MissingFromDisk mode, missing_seasons, and no_u…
machetie Jun 18, 2026
d523e79
feat: add post-repair grab verification from Pukabyte/repair
machetie Jun 18, 2026
ff6c516
feat: skip still-airing seasons in missing_seasons check
machetie Jun 18, 2026
614567e
fix: address augmentcode review suggestions
machetie Jun 18, 2026
74ffe66
feat(repair): rewrite repair check with API-first dead-symlink detection
machetie Jun 19, 2026
eb34fd5
feat(repair): add per-sweep symlink cap and raise default action limit
machetie Jun 19, 2026
dbf41ec
feat(scheduler): run checks on independent fast/slow intervals
machetie Jun 19, 2026
6bbafb3
refactor: split monolithic doctor.py into a package
machetie Jun 19, 2026
1a69d2d
fix(webui): qualify _warm_recent/_warm_count with _warmer module
machetie Jun 19, 2026
db9de60
fix(startup): fail fast when instance-dependent checks have no instances
machetie Jun 19, 2026
9f524c3
feat(missing_seasons): faster backlog clearing + backfill mode
machetie Jun 19, 2026
a215668
feat(janitor): scan decypharr logs for operational errors + API probe
machetie Jun 19, 2026
628fcc7
fix(janitor): support altmount/complete symlink targets and add patch…
Jun 19, 2026
93fd269
refactor: logging, repair split, retry, and orphan scan
Jun 19, 2026
0d63f4a
fix(janitor): suppress false-positive 401/403/429 from hash/ID substr…
Jun 22, 2026
fecdd0d
feat(decypharr): robust 3-layer FUSE mount health check with strike c…
Jun 22, 2026
e0e660a
feat(missing_seasons): search partial seasons that have fully aired
Jun 22, 2026
6c1a1ca
feat: add debug logging across all checks + fix env-over-config priority
Jun 22, 2026
05ef217
feat: auto-escalate to SeriesSearch for multi-season packs when Seaso…
Jun 22, 2026
bfb1c74
feat: check_multipack — auto-push cached multi-season packs that Sona…
Jun 22, 2026
85fc22d
feat: multipack — filter by overlap with incomplete seasons, rank by …
Jun 22, 2026
bca81d6
refactor: code review fixes — scheduler log, O(1) cache lookup, state…
Jun 22, 2026
2a9367b
fix: __main__.py 4-tuple unpack broken after CHECKS widened to 5-tuple
Jun 22, 2026
2e2696b
refactor: Phase 0 — bug fixes, CheckEntry namedtuple, Arr methods, te…
Jun 22, 2026
51562a9
test: Phase 1 — characterization tests for repair, janitor, plexscan
Jun 22, 2026
7d97895
refactor: Phase 1 — reset-able state objects for decypharr + plexscan
Jun 22, 2026
21cc013
refactor: P2.1 — extract utility helpers from config.py into doctor/u…
Jun 22, 2026
bc8d9c4
refactor: P2.1 — move http_code, run_cmd, run_output, host_load to do…
Jun 22, 2026
97b1f2c
refactor: P2.2 — add needs_instances to CheckEntry and derive _needs_…
Jun 22, 2026
01c62e0
refactor: P3.1 — replace star-import in scheduler.py with explicit ch…
Jun 22, 2026
702a30b
refactor: P3.3 — replace star-imports in __main__.py with explicit im…
Jun 22, 2026
42cf9bc
test: add compatibility tests for doctor.checks exports
Jun 22, 2026
00e10a5
refactor: convert doctor/checks/__init__.py star-imports to explicit …
Jun 22, 2026
cfe0114
refactor: remove redundant from .checks import * in webui.py
Jun 22, 2026
7b6ffbe
refactor: P4 — explicit imports in scheduler, clients, state, webui
Jun 22, 2026
468e815
refactor: P5 — explicit imports in all check modules (eliminates all …
Jun 22, 2026
9bd4148
refactor: add __all__ to webui.py and scheduler.py (documentation-only)
Jun 22, 2026
d690001
ci: add test + lint + smoke gates; fix two import bugs found by ruff
Jun 22, 2026
f09d0ba
tests: 109 new tests for seerr, repair/*, warmer (Phases 4-7)
Jun 22, 2026
19b4d4f
fix+obs: A1-A4 correctness fixes and B1-B4 observability
Jun 22, 2026
e54746c
Fix ruff F401 unused import warnings in test files and clients.py
Jun 22, 2026
c3a716c
feat: file-level janitor + janitor/repair coordination for hierarchic…
Jul 1, 2026
f549634
feat(maintainer): auto-delete ended/unwatched shows from Sonarr + Pul…
machetie Aug 9, 2026
faf88e5
fix(config): resolve merge artifact in maintainer constants
machetie Aug 9, 2026
327242f
maintainer: add Pulsarr exclusion integration with root folder filter
machetie Aug 9, 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: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ SONARR_API_KEY=
RADARR_API_KEY=
SONARR4K_API_KEY=
RADARR4K_API_KEY=
PROWLARR_API_KEY=
BAZARR_API_KEY=
SEERR_API_KEY=
PLEX_TOKEN=
122 changes: 122 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: ci

on:
push:
branches: ["**"]
pull_request:

jobs:
# ── 1. Unit tests ────────────────────────────────────────────────────────
test:
name: Unit tests (Python 3.12)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Run test suite
run: python3 -m unittest discover -s tests -v

# ── 2. Syntax / lint gate ────────────────────────────────────────────────
lint:
name: Syntax + lint (compileall + ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Compile check (syntax errors)
run: python3 -m compileall -q doctor/

- name: Install ruff
run: pip install --quiet ruff

- name: Ruff — undefined names and unused imports (F-only)
run: ruff check doctor/ tests/ --output-format=github

# ── 3. Startup + /healthz smoke ─────────────────────────────────────────
smoke:
name: Startup smoke + /healthz
runs-on: ubuntu-latest
needs: [test, lint]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Start daemon (no checks, UI enabled)
run: |
ENABLE_QUEUE=false \
ENABLE_MULTIPACK=false \
ENABLE_UI=true \
DOCTOR_UI_PORT=12345 \
DOCTOR_STATE_FILE=/tmp/doctor_state.json \
python3 -m doctor &
echo $! > /tmp/doctor.pid

- name: Wait for HTTP server to be ready
run: |
for i in $(seq 1 10); do
if curl -sf http://127.0.0.1:12345/healthz; then
echo " — /healthz OK"
exit 0
fi
sleep 0.5
done
echo "ERROR: /healthz did not respond within 5s"
exit 1

- name: Tear down daemon
if: always()
run: kill $(cat /tmp/doctor.pid) 2>/dev/null || true

# ── 4. Docker publish (main + tags only, gated on test + lint + smoke) ──
publish:
name: Build and push image
runs-on: ubuntu-latest
needs: [test, lint, smoke]
if: |
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag
type=sha,format=short

- name: Set up Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
48 changes: 10 additions & 38 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -1,45 +1,17 @@
name: publish image
# This workflow has been superseded by ci.yml which runs tests, lint, and smoke
# before publishing. The publish job now lives in ci.yml as the final gated step.
# This file is retained for reference only and has no triggers.
name: publish image (retired)

on:
push:
branches: [ main ]
tags: [ 'v*' ]
workflow_dispatch:
inputs:
reason:
description: "Reason (this workflow is retired — use ci.yml)"
required: false

jobs:
build-and-push:
notice:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag
type=sha,format=short

- name: Set up Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- run: echo "This workflow is retired. Publishing is handled by ci.yml."
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Agent Notes

This project is a pure-Python package (no third-party dependencies).

## How to run

```bash
python3 -m doctor
```

## How to run tests

```bash
python3 -m unittest discover -s tests -v
```

## Quick smoke test

```bash
ENABLE_QUEUE=false ENABLE_UI=true DOCTOR_UI_PORT=12345 DOCTOR_STATE_FILE=/tmp/doctor_state.json timeout 3 python3 -m doctor
```

## How to deploy

- Docker: `COPY doctor /app/doctor` and `ENTRYPOINT ["python3", "-m", "doctor"]`.
- Host systemd: copy the `doctor/` package directory, set `WorkingDirectory` to that directory, and run `python3 -m doctor`.
9 changes: 5 additions & 4 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Deployment guide

stack-doctor is one small file (`doctor.py`, pure Python standard library, no dependencies).
stack-doctor is a small pure-Python package (standard library only, no third-party dependencies).
Pick the path that matches your setup:

- **Docker** is easiest. It runs the `queue` / `providers` / `plex` / `resources` checks and the
Expand Down Expand Up @@ -69,8 +69,9 @@ Run this on the machine that runs decypharr.

```bash
mkdir -p /opt/stack-doctor
curl -fsSL https://raw.githubusercontent.com/Neoo-Blue/stack-doctor/main/doctor.py \
-o /opt/stack-doctor/doctor.py
cd /opt/stack-doctor
curl -fsSL https://github.com/Neoo-Blue/stack-doctor/archive/refs/heads/main.tar.gz \
| tar -xz --strip-components=1 stack-doctor-main/doctor
```

2. Install the service template and edit the values (URLs, API keys, paths):
Expand Down Expand Up @@ -115,6 +116,6 @@ and open `http://<host>:12345/?token=something`.
## Updating

- **Docker**: `docker compose pull && docker compose up -d`
- **Host**: re-download `doctor.py` (step 1 above) and `systemctl restart stack-doctor`
- **Host**: re-download/extract the `doctor` package (step 1 above) and `systemctl restart stack-doctor`

Your saved settings live in `DOCTOR_CONFIG_FILE` (`/data/config.json` by default) and survive updates.
19 changes: 13 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@ ENV PYTHONUNBUFFERED=1 \
DOCTOR_STATE_FILE=/data/state.json

WORKDIR /app
COPY doctor.py /app/doctor.py
COPY doctor /app/doctor

# No Python dependencies (standard library only). openssh-client lets a restart
# hook reach a *host* service (e.g. DECYPHARR_RESTART_CMD="ssh root@host systemctl restart decypharr").
# The doctor package uses only the Python standard library. openssh-client lets a
# restart hook reach a *host* service (e.g. DECYPHARR_RESTART_CMD="ssh root@host systemctl restart decypharr").
# docker-ce-cli lets a restart hook control local containers via a bind-mounted /var/run/docker.sock.
# Runs as root so a bind-mounted /data (and an optional rw /mnt/library for the
# janitor) is always writable regardless of host ownership.
RUN apt-get update \
&& apt-get install -y --no-install-recommends openssh-client \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get install -y --no-install-recommends openssh-client ca-certificates curl gnupg \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \
&& chmod a+r /etc/apt/keyrings/docker.asc \
&& echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends docker-ce-cli \
&& rm -rf /var/lib/apt/lists/* /etc/apt/keyrings /etc/apt/sources.list.d/docker.list \
&& mkdir -p /data
VOLUME /data

# webhook port (event mode) + web dashboard (ENABLE_UI)
EXPOSE 8088 12345

ENTRYPOINT ["python3", "/app/doctor.py"]
ENTRYPOINT ["python3", "-m", "doctor"]
Loading