Compare commits

...

11 Commits

Author SHA1 Message Date
dependabot[bot] edba71f470 chore(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 15:39:35 +00:00
rUv 55f6a74e1e Merge pull request #913 from ruvnet/fix/ci-v1-api-perms-locust
ci(v1-api): fix gh-pages 403 + run real pytest perf suite
2026-06-02 17:36:43 +02:00
ruv b5a91c5635 ci(v1-api): install pytest, drop root --cov addopts for perf suite, ascii comment 2026-06-02 17:29:04 +02:00
ruv 308d2fc89d ci(v1-api): fix gh-pages 403 + run real perf suite — green main CI
Two more latent v1-API CI bugs surfaced once #910/#911 let the jobs reach
their later steps:

- API Documentation: openapi generation now succeeds (psutil fix), but the
  gh-pages deploy failed with HTTP 403 — the job had no `permissions` block
  and GITHUB_TOKEN is read-only by default. Add `permissions: contents:
  write`, and make the deploy `continue-on-error` (the openapi generation is
  the real validation; Pages may be disabled).
- Performance Tests: ran `locust -f tests/performance/locustfile.py`, but
  there is no locustfile — the suite is pytest (test_api_throughput.py,
  test_frame_budget.py, test_inference_speed.py). Run pytest instead, with
  working-directory: archive/v1 and MOCK_POSE_DATA=true.

ci.yml validated as well-formed YAML.
2026-06-02 17:26:39 +02:00
rUv 5038e3c8e1 Merge pull request #911 from ruvnet/fix/ci-v1-api-mock-mode
ci(v1-api): MOCK_POSE_DATA + declare psutil — green Performance Tests & API Docs
2026-06-02 06:20:21 -04:00
ruv e239af3636 fix(deps): declare psutil in requirements.txt — green API Documentation CI
The API Documentation job (and any env without locust) failed with
`ModuleNotFoundError: No module named 'psutil'` when importing the app:
psutil is imported by src/api/routers/health.py, services/metrics.py,
commands/status.py, and tasks/monitoring.py, but was never declared as a
dependency — it only happened to be present where locust (Performance
Tests) pulled it in transitively. Declare it explicitly (psutil>=5.9.0).

Verified locally: `from src.api.main import app; app.openapi()` (the exact
docs-job operation) now succeeds.
2026-06-02 12:11:55 +02:00
ruv 4856afbd0c ci(v1-api): run Performance Tests + API Docs with MOCK_POSE_DATA=true
After the DensePoseHead startup fix (#910), the v1 API starts, but the
Performance Tests load-hit the pose endpoints which error "requires real
CSI data" (no hardware in CI, mock_pose_data defaults False), and the
API-docs job imports the app the same way. Set MOCK_POSE_DATA=true on both
jobs so they exercise the mock path. Verified: the env var maps to
settings.mock_pose_data=True (pydantic, no env_prefix).

(Note: Performance Tests is continue-on-error so this is cleanup, not a
run-blocker; the run-level red on main has been transient Docker Hub pull
timeouts on Tests/docker-build, which are infra flakes that pass on re-run.)
2026-06-02 12:04:58 +02:00
rUv 4d205a05c4 Merge pull request #910 from ruvnet/fix/v1-pose-service-densepose-config
fix(v1-api): pass required config to DensePoseHead — green main CI
2026-06-02 05:50:25 -04:00
ruv bc42ae7903 fix(v1-api): pass required config to DensePoseHead — green main CI
The "Continuous Integration" workflow (Performance Tests + API
Documentation jobs) has failed on every main commit since the API start
path was exercised: pose_service._initialize_models() called
`DensePoseHead()` with no args, but DensePoseHead.__init__ requires a
config dict → "TypeError: DensePoseHead.__init__() missing 1 required
positional argument: 'config'" → uvicorn "Application startup failed".

Pass a config: input_channels=256 (matches the modality translator's
output), num_body_parts=24 (DensePose standard), num_uv_coordinates=2.
Both call sites (with/without pose_model_path) fixed.

Verified locally: DensePoseHead(config) + ModalityTranslationNetwork(config)
both construct + eval, clearing the startup TypeError.
2026-06-02 11:42:52 +02:00
rUv b7b8c1109b Merge pull request #908 from ruvnet/fix/893-release-bins-refresh
release(firmware): refresh release_bins with the #893 CSI fix → v0.6.7
2026-06-02 05:35:34 -04:00
ruv 786e834dae release(firmware): refresh release_bins with the #893 CSI fix → v0.6.7
The pre-built binaries in release_bins/ were v0.6.6 (May 21) and shipped
the MGMT-only promiscuous filter, so display-less boards flashed from them
got yield=0pps (#893/#866/#897 — the root cause of the "can't reproduce /
it's fake" reports). Rebuilt every flashable variant from main (which has
the #893 display-gated DATA-frame fix) and refreshed the binaries:

- top-level ESP32-S3 8MB (sdkconfig.defaults) — esp32-csi-node.bin +
  bootloader (partition-table/ota_data unchanged — code-only fix)
- esp32-csi-node-4mb.bin (ESP32-S3 4MB, sdkconfig.defaults.4mb)
- c6-adr110/ (ESP32-C6, sdkconfig.defaults.esp32c6) — the exact firmware
  hardware-verified on COM6 (CSI yield 0→27 pps, presence/motion alive,
  no #396 crash)
- s3-adr110/ (same production S3 8MB config)

Left untouched: s3-fair-adr110/ (a non-production size-comparison build,
features stripped — not a board anyone flashes for sensing).

version.txt → 0.6.7; SHA256SUMS regenerated for the changed variant dirs.
Display boards keep MGMT-only (preserves the #396 crash protection);
display-less boards now capture DATA frames and stream CSI.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-02 11:18:03 +02:00
35 changed files with 91 additions and 61 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
run:
working-directory: v2
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Rust toolchain
run: rustup show && rustc --version
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
+3 -3
View File
@@ -41,7 +41,7 @@ jobs:
image_tag: ${{ steps.determine-tag.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Determine deployment environment
id: determine-env
@@ -85,7 +85,7 @@ jobs:
url: https://staging.wifi-densepose.com
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up kubectl
uses: azure/setup-kubectl@v3
@@ -131,7 +131,7 @@ jobs:
url: https://wifi-densepose.com
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up kubectl
uses: azure/setup-kubectl@v3
+28 -9
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -81,7 +81,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
# `wifi-densepose-desktop` is a Tauri v2 app — `glib-sys`, `gtk-sys`,
# `webkit2gtk-sys`, etc. need the Linux dev libraries via pkg-config or the
@@ -188,7 +188,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
continue-on-error: true
@@ -253,7 +253,7 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
@@ -265,23 +265,37 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install locust
pip install pytest # the perf suite is pytest, not locust
- name: Start application
working-directory: archive/v1
env:
# No CSI hardware in CI — serve mock pose data so the pose endpoints
# respond 200 under load instead of erroring "requires real CSI data".
MOCK_POSE_DATA: "true"
run: |
uvicorn src.api.main:app --host 0.0.0.0 --port 8000 &
sleep 10
- name: Run performance tests
working-directory: archive/v1
env:
MOCK_POSE_DATA: "true"
run: |
locust -f tests/performance/locustfile.py --headless --users 50 --spawn-rate 5 --run-time 60s --host http://localhost:8000
# The repo's performance suite is pytest (test_api_throughput.py,
# test_frame_budget.py, test_inference_speed.py) — there is no
# locustfile.py, so the old `locust -f tests/performance/locustfile.py`
# command always failed with "Could not find ...". Run the real suite.
# -o addopts="" drops the root pyproject's --cov/--cov-fail-under=100
# flags (pytest-cov isn't installed here and 100% cov is for unit tests).
pytest tests/performance/ -o addopts="" -v --junitxml=perf-junit.xml
- name: Upload performance results
if: always()
uses: actions/upload-artifact@v4
with:
name: performance-results
path: locust_report.html
path: archive/v1/perf-junit.xml
# Docker Build and Test
# NOTE: the canonical Docker build for the sensing-server is now
@@ -299,7 +313,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Docker Buildx
continue-on-error: true
@@ -367,9 +381,11 @@ jobs:
runs-on: ubuntu-latest
needs: [docker-build]
if: github.ref == 'refs/heads/main'
permissions:
contents: write # gh-pages deploy needs write (GITHUB_TOKEN is read-only by default -> 403)
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
@@ -384,6 +400,8 @@ jobs:
- name: Generate OpenAPI spec
working-directory: archive/v1
env:
MOCK_POSE_DATA: "true" # no CSI hardware in CI
run: |
python -c "
from src.api.main import app
@@ -394,6 +412,7 @@ jobs:
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
continue-on-error: true # openapi generation above is the real validation; deploy is best-effort (Pages may be disabled)
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
snapshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Fetch /traffic/clones + /traffic/views from GitHub
env:
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
name: Build x86_64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
@@ -77,7 +77,7 @@ jobs:
name: Build aarch64 (arm)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
@@ -144,7 +144,7 @@ jobs:
github.event_name == 'push' &&
vars.HAS_GCP_CREDENTIALS == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download x86_64 artifact
uses: actions/download-artifact@v4
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with: { targets: wasm32-unknown-unknown }
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Rust + wasm32 target
uses: dtolnay/rust-toolchain@stable
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
target: [aarch64-apple-darwin, x86_64-apple-darwin]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -82,7 +82,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -130,7 +130,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Download all artifacts
uses: actions/download-artifact@v4
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
if: github.ref_type == 'tag'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Check firmware version.txt == tag
run: |
# Tag form: vX.Y.Z-esp32 → expect version.txt to contain X.Y.Z
@@ -70,7 +70,7 @@ jobs:
artifact_pt: partition-table-c6.bin
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build firmware (${{ matrix.variant }})
working-directory: firmware/esp32-csi-node
+4 -4
View File
@@ -99,7 +99,7 @@ jobs:
- boundary-min
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download QEMU artifact
uses: actions/download-artifact@v4
@@ -213,7 +213,7 @@ jobs:
name: Fuzz Testing (ADR-061 Layer 6)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install clang
run: |
@@ -262,7 +262,7 @@ jobs:
name: NVS Matrix Generation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install NVS generator
run: pip install esp-idf-nvs-partition-gen
@@ -316,7 +316,7 @@ jobs:
image: espressif/idf:v5.4
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download QEMU artifact
uses: actions/download-artifact@v4
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
name: Verify fix markers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
RUST_BACKTRACE: 1
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install mosquitto + clients and start with allow_anonymous
run: |
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v3
+3 -3
View File
@@ -75,7 +75,7 @@ jobs:
arch: AMD64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
# Linux aarch64 needs QEMU for cross-build on x86_64 runners.
- name: Set up QEMU
@@ -120,7 +120,7 @@ jobs:
startsWith(github.ref, 'refs/tags/v2.')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install maturin
run: pip install maturin>=1.7
- name: Build sdist
@@ -143,7 +143,7 @@ jobs:
startsWith(github.ref, 'refs/tags/v1.99')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: '3.12'
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Stage viewer for Pages
run: |
+4 -4
View File
@@ -39,7 +39,7 @@ jobs:
- { label: 'ruflo+itar', flags: '--features ruflo,itar-unrestricted' }
- { label: 'full+train', flags: '--features full,train' }
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
@@ -59,7 +59,7 @@ jobs:
name: clippy (-D warnings, --no-deps)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
# v2/rust-toolchain.toml pins channel "1.89" with profile "minimal" (no
# clippy). dtolnay@stable installs clippy on the floating "stable"
# toolchain, but the override makes cargo use the separate "1.89"
@@ -92,7 +92,7 @@ jobs:
name: build train_marl bin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
@@ -126,7 +126,7 @@ jobs:
name: ITAR / publish guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: publish = false is present (no accidental crates.io publish)
run: |
CARGO=v2/crates/ruview-swarm/Cargo.toml
+7 -7
View File
@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -95,7 +95,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python
continue-on-error: true
@@ -162,7 +162,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Docker Buildx
continue-on-error: true
@@ -243,7 +243,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Run Checkov IaC scan
continue-on-error: true
@@ -304,7 +304,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -339,7 +339,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python
continue-on-error: true
@@ -376,7 +376,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Check security policy files
continue-on-error: true
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
name: build · push · smoke-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Stage demos for Pages
run: |
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: true
fetch-depth: 0
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
+12 -3
View File
@@ -107,16 +107,25 @@ class PoseService:
async def _initialize_models(self):
"""Initialize neural network models."""
try:
# Initialize DensePose model
# Initialize DensePose model. DensePoseHead requires a config
# dict — input_channels matches the modality translator's output
# (256), with the standard DensePose 24 body parts and 2 (U,V)
# coordinates. (Previously called with no args → TypeError at
# startup, which broke the API service.)
densepose_config = {
'input_channels': 256,
'num_body_parts': 24,
'num_uv_coordinates': 2,
}
if self.settings.pose_model_path:
self.densepose_model = DensePoseHead()
self.densepose_model = DensePoseHead(densepose_config)
# Load model weights if path is provided
# model_state = torch.load(self.settings.pose_model_path)
# self.densepose_model.load_state_dict(model_state)
self.logger.info("DensePose model loaded")
else:
self.logger.warning("No pose model path provided, using default model")
self.densepose_model = DensePoseHead()
self.densepose_model = DensePoseHead(densepose_config)
# Initialize modality translation
config = {
Binary file not shown.
@@ -1,4 +1,4 @@
889715e9d698ad78f9978ad8b93b6af24a726b0494247201c8f0d920d9fc80ca *firmware/esp32-csi-node/release_bins/c6-adr110/bootloader.bin
d8539e47c6f10a3344679118619e3fe01cfd66eb560ea8883268ca7c9a12efa4 *firmware/esp32-csi-node/release_bins/c6-adr110/esp32-csi-node.bin
b0fb1f217a39c80bc95b5eb8208a0b8572ae64efa0f6d580b76caff4affe0f4d *firmware/esp32-csi-node/release_bins/c6-adr110/bootloader.bin
4764c5b20a353895f70122816adc98f861ec20e9a8ea9b344dc0648b6341073c *firmware/esp32-csi-node/release_bins/c6-adr110/esp32-csi-node.bin
7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f *firmware/esp32-csi-node/release_bins/c6-adr110/ota_data_initial.bin
4c2cc4ffd52641e23b779bd57b3908014083ac3c1aab395756478c89e70d81f0 *firmware/esp32-csi-node/release_bins/c6-adr110/partition-table.bin
@@ -1,3 +1,3 @@
3c4905dd202ccabf4230cbabcc9320f250a60b1a7254eff7424780201bcb2072 *firmware/esp32-csi-node/release_bins/s3-adr110/bootloader.bin
7a8bf9582c9031fed32f1ada44f5c41dd99bd07fadff8e5c86e07aa0f343e847 *firmware/esp32-csi-node/release_bins/s3-adr110/esp32-csi-node.bin
b973d7eda65affb746adcfa63ceb18f779f206d240b76f01b8c9ae7485455660 *firmware/esp32-csi-node/release_bins/s3-adr110/bootloader.bin
e21ef94aba779d534dc048c1b9da731c81e5dbe09d0645cfd70a05ad3642d3e9 *firmware/esp32-csi-node/release_bins/s3-adr110/esp32-csi-node.bin
67222c257c0477501fd4002275638dc4262b34eb68235b8289fb1337054d322b *firmware/esp32-csi-node/release_bins/s3-adr110/partition-table.bin
@@ -1,3 +1,4 @@
0.6.6
git-sha: cbcb389cb (pre-commit)
built: 2026-05-21
0.6.7
git-sha: 8703ade9b
built: 2026-06-02
note: RuView#893 — display-less boards capture DATA frames (CSI yield 0pps fix); hardware-verified on ESP32-C6 (0->27 pps)
+1
View File
@@ -36,3 +36,4 @@ scikit-learn>=1.2.0
# Monitoring dependencies
prometheus-client>=0.16.0
psutil>=5.9.0 # system metrics — imported by health.py / metrics.py / status.py / monitoring.py