- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The previous commit called .claude/ an "empty leftover". It held 75 files across three locked git worktrees. I wrote that before checking, which is the exact failure the verification hook warns about. The conclusion was still right, now actually verified: all three worktree branches had 0 commits not already in main, and main is fully pushed to Forgejo. Nothing was at risk of being lost. Now properly cleaned up rather than just ignored -- the three worktrees are removed, their merged worktree-agent-* branches deleted, and git worktree prune run. .claude/ is empty on disk and stays gitignored. qui-monitor was the only repo in the workspace carrying stale agent worktrees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Nk7xhvv9mrDNHNrNkFggG |
||
| .planning | ||
| bin | ||
| cards | ||
| catalog | ||
| docs | ||
| plugins/qui | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| README.md | ||
qui-monitor
What this is
A Manifold plugin + publisher daemon that surfaces Qui (a multi-instance qBittorrent aggregator) as dashboard cards in Manifold. The plugin is the first consumer of Manifold v1.2's parameterized-leaf capability — proving that the new "per-instance leaves at runtime" abstraction works end-to-end against a real, dynamic backend.
Karl runs four qBittorrent instances behind Qui to keep individual seed counts manageable (qBit's web UI degrades past a few thousand torrents per instance). The fleet currently has no per-instance visibility in Manifold; this product closes that gap.
What it doesn't do
- No action endpoints. No pause / resume / delete buttons. Read-surface only — torrent state as observed is fine; the act of pausing happens in Qui's own UI. This is a project-wide line per
feedback_no_shell_via_browser. - No Qui fork. We deploy Qui upstream and observe it. Issues with Qui itself go to
autobrr/qui. - No new Manifold core. Everything here is a plugin; Manifold core stays vendor-clean.
Architectural decisions (locked, from 2026-05-17 /gsd-explore)
Full rationale: .planning/notes/qui-plugin-architecture-exploration.md. Five-decision summary:
- Read-surface only — no action endpoints on the dashboard. Project-wide "no shell from browser" rule.
- Both transports, split by leaf shape — Prometheus (port 9074) for scalars + time-series; JSON API (port 7476,
X-API-Key) for lists, tables, per-torrent detail, instance management. When both transports expose the same datum, Prometheus wins (cheap, pre-aggregated). Seedocs/ARCHITECTURE.mdfor the full walk. - Publisher-daemon pattern, never direct-from-browser —
manifold-qui.serviceon a server owns both transports and republishes to MQTT. Credentials stay in/etc/default/manifold-qui; the dashboard never sees them. - Both aggregate AND per-instance leaves — aggregates for fleet health, per-instance for drill-down. Per-instance branch is dynamic.
- Parameterized template leaves with picker-driven resolution — the leaf id is a template (
qui.instance.{instance}.dl_speed), the wizard renders a picker beside it populated fromqui.instances, the card stores the resolved internal ID, the renderer rehydrates the human label at display time. This is the v1.2 manifold capability this product validates end-to-end.
Core value
A non-engineer with N qBit instances behind Qui should be able to build dashboard cards that don't break when an instance is renamed or removed. Everything in this repo serves that.
What this product owns
qui-monitor/
├── plugins/qui/ # the Manifold plugin
│ ├── manifest.yaml
│ ├── catalog.template.json # leaves (aggregate + parameterized per-instance)
│ ├── publisher.py # the daemon: Prom scrape + JSON API → MQTT
│ ├── manifold-qui.service # systemd unit
│ └── etc-default-qui.example # credential template (real creds: server only)
├── consolidator/ # empty for now; reserved if a Netdata bridge becomes useful
├── catalog/
│ └── leaves.json # frozen reference copy of the catalog (review surface)
├── cards/
│ └── qui-monitor-cards.json # bundled card recipes (fleet glance, per-instance health row, etc.)
├── templates/ # wizard preset templates if/when those become a thing
├── docs/
│ ├── ARCHITECTURE.md # how the daemon works internally
│ ├── HARDWARE.md # n/a — Qui is software-only; the file records the upstream-service inventory
│ ├── SECRETS.md # what creds, where stored, rotation procedure
│ ├── GOTCHAS.md # known issues + workarounds
│ └── RUNBOOK-*.md # rotate API key, recover from Qui restart, etc.
└── README.md # why, what it owns, deploy summary
What this product does not own
- Qui itself — that's
autobrr/quiupstream. We deploy it but don't fork it. - qBittorrent instances — they live in their own LXCs (Karl runs 4 qBit instances on LXC 101 per
../CLAUDE.md). Those are separately operated; this product just observes via Qui. - Manifold core (catalog system, cards renderer, parameterized-leaf capability) — lives in
../manifold/. This product consumes; Manifold core stays vendor-clean. - The MQTT broker — mosquitto on LXC 109. Existing infra.
- Action endpoints — pause / resume / delete / set-category. Hard line per Decision 1.
- Authentication for cards — Manifold's existing Authentik forward-auth posture covers it.
Prerequisites
Three things must already exist before the Quick start makes sense. None of them are owned by this product, and each has its own upstream documentation.
-
Qui upstream installed and reachable. Get it from https://github.com/autobrr/qui and configure it against your qBit instances. "Reachable" means: from the host that will run
manifold-qui.service, this curl returns JSON (not 401, not connection-refused) — replace placeholders with your own values:curl http://<your-qui-host>:7476/api/instances/ -H "X-API-Key: <your-key>"If you see
401, the API key is wrong; if you see connection refused, the port is wrong or Qui isn't listening on it. -
Manifold v1.2+ deployed and reachable. v1.2 is the version that introduced the parameterized-leaf capability this plugin consumes. See
../manifold/.planning/milestones/v1.2-ROADMAP.mdfor the v1.2 scope. The Manifold backend's/api/v0/cards/import-previewand/api/v0/cards/importendpoints both arrive in v1.2; without them, the bundle-import step below has nothing to call. -
mosquitto broker reachable from the host running the publisher (default port 1883). Worked-example value on Karl's homelab:
192.168.51.12:1883on LXC 109. Replace with your own broker's address in/etc/default/manifold-qui.
Quick start
Seven steps. Steps 1–5 deploy the publisher; step 6 verifies the plugin reaches Manifold; step 7 builds a first card via the wizard. To skip step 7's manual card-build and import the three bundled recipes instead, see the next section (## Importing the bundled card recipes).
-
Clone this repo to the host that will run the publisher daemon. Karl's homelab puts it on LXC 111 (the Manifold backend host, co-located by convenience — no architectural requirement). Replace with your own host.
git clone <repo-url> /opt/manifold/plugins/qui-monitor cd /opt/manifold/plugins/qui-monitor -
Place the plugin tree where Manifold's loader looks for it. If the clone path above already puts it under
/opt/manifold/plugins/, skip this step.cp -r plugins/qui /opt/manifold/plugins/ -
Seed the credential file from the template, then edit in your real values.
install -m 0600 -o root -g root plugins/qui/etc-default-qui.example /etc/default/manifold-quiEdit
/etc/default/manifold-quiand set:QUI_URL,QUI_API_KEY,QUI_PROM_URL, optionallyQUI_PROM_USER+QUI_PROM_PASS(both blank or both set perdocs/SECRETS.md),MQTT_HOST,MQTT_PORT. Detailed sourcing for each credential is indocs/SECRETS.md. To rotate any of them later, seedocs/RUNBOOK-rotate-api-key.mdordocs/RUNBOOK-rotate-prom-basic-auth.md. -
Install the systemd unit:
cp plugins/qui/manifold-qui.service /etc/systemd/system/ systemctl daemon-reload systemctl enable --now manifold-qui -
Verify the publisher is healthy. The publisher emits one INFO heartbeat per 60 seconds (per
docs/ARCHITECTURE.md## Heartbeat). Wait one heartbeat cycle, then:journalctl -u manifold-qui -n 5 --no-pagerThe expected line contains
prometheus_ok=true json_api_ok=true. If either flag readsfalse, seedocs/SECRETS.md## Failure modes for credential checks, ordocs/GOTCHAS.mdfor upstream-side reasons. If neither flag has flipped totrueafter two heartbeats (~120s), something is keeping the publisher from completing a first poll — likely a wrong URL in step 3. -
Confirm the plugin appears in Manifold. The Manifold backend re-scans
plugins/at startup; if it was running while you copied the tree in step 2, restart it. Karl's homelab:systemctl restart manifoldon LXC 111 (replace with your own). Open the dashboard and check the plugin list (gear → System → Plugins);quishould appear with its leaves enumerated. -
Build your first card via the wizard. In the dashboard's card-builder, add a card whose source is
qui.instance.{instance}.dl_speed. The wizard renders a picker beside the leaf id, populated from your livemetrics/qui/instancestopic — pick one of your instances and save. Live dl-speed should render within one publish cycle (≤30s default).
To import the three bundled recipes instead of hand-building, see the next section.
Importing the bundled card recipes
qui-monitor ships three prebuilt card recipes in cards/qui-monitor-cards.json — fleet glance, per-instance health row, and instance drill-down — using Manifold's portable manifold_card_bundle: 1 format (Phase 2 D-01). Importing them lands the cards in your dashboard in one operation. This is the canonical install path for the bundled recipes; nothing else owns it.
The two endpoints are owner-only (_require_owner in ../manifold/core/backend/server.py around lines 2669-2741). The X-Auth-Username header is how Karl's Authentik forward-auth carries identity into the backend — replace with whatever your auth scheme expects. A reader with no owner role gets a 403.
Step 1 — Preview (validates the bundle and tells you what'll happen on import — no state is changed):
curl -X POST http://<manifold-backend>:<port>/api/v0/cards/import-preview \
-H "Content-Type: application/json" \
-H "X-Auth-Username: <owner-username>" \
--data-binary @cards/qui-monitor-cards.json
The response is a per-card outcome list. Each card carries an outcome field; the values you might see (definitive list in ../manifold/docs/CARD-PORTABILITY.md §"Resolution rules (import-time)"):
ok— card resolves cleanly; will import without input.prompt_needed— multiple instances match; you'll pick when you commit.instance_missing— the bundle pins an instance id you don't have. Seedocs/GOTCHAS.md## Instance-id portability gap for the substitution procedure. This is the most likely first-import speed bump on a non-Karl deploy — the per-instance health row and instance drill-down recipes pin Karl's real instance ids.metric_missing— the bundle uses a metric your catalog doesn't expose. Means the plugin is out of date relative to the bundle; reinstall the plugin (re-run step 2 of Quick start) and re-preview.
Step 2 — Import (commits the cards into your dashboard state):
curl -X POST http://<manifold-backend>:<port>/api/v0/cards/import \
-H "Content-Type: application/json" \
-H "X-Auth-Username: <owner-username>" \
-d "$(jq -n --slurpfile bundle cards/qui-monitor-cards.json '{bundle: $bundle[0], picks: {}}')"
picks is the only body field that varies between deploys: it's a map of {"<card-index>": "<instance-id>"} covering any cards that step 1 returned as prompt_needed. Cards step 1 marked ok import straight through; cards marked instance_missing won't be in the bundle anymore (you've already substituted via the GOTCHAS walk before reaching step 2).
The bundle was generated from Karl's live dashboard state via bin/state-to-bundle.py (Phase 2's authoring tooling). Curious readers can audit the recipe build path there.
How it's operated
Once deployed, qui-monitor is mostly hands-off. The four reasons to revisit it:
- Where did this byte come from? →
docs/ARCHITECTURE.mdwalks every leaf back to its transport hop. - Credentials need rotating, or someone's asking about secret-handling? →
docs/SECRETS.md. - Something surprising happened? →
docs/GOTCHAS.mdlists the five named items + a deferred-ideas section. - Need to do a specific recovery procedure? →
docs/RUNBOOK-rotate-api-key.md,docs/RUNBOOK-rotate-prom-basic-auth.md,docs/RUNBOOK-recover-from-qui-restart.md,docs/RUNBOOK-broker-outage.md. The runbooks share a voice template; each leads with the failure mode that justifies its existence.
Audience
v1: Karl. Specifically: Karl with 4 qBit instances behind Qui, wanting per-instance dl/ul speed + torrent counts on the dashboard without manually maintaining four near-identical card sets.
v2 (aspirational): any homelab operator running multi-instance qBit through Qui. The publisher-daemon pattern (Decision 3) is partly future-proofing for this — direct-from-browser would have broken at the second user.
Success criteria (v1.0 ship test)
A non-engineer holding this README can:
- Install Qui upstream and configure it with their qBit instance(s) (out of scope but documented as Prerequisite #1).
git clonethis repo, copyetc-default-qui.example→/etc/default/manifold-quiwith their Qui URL + API key,systemctl enable --now manifold-qui.service.- See the Qui plugin appear in Manifold's plugin list.
- Use the wizard to build a card: pick a parameterized leaf (e.g.
qui.instance.{instance}.dl_speed), select an instance from the live picker, save. - See the card render with live data, the right human label, and updated speed numbers.
- Rename the instance in Qui — the card label updates within one publish cycle, data keeps flowing, no card edit needed (latency floor:
docs/GOTCHAS.md## Instance-rename label latency). - Remove the instance in Qui — the card transitions to the missing-instance graceful-degrade tile, other cards unaffected.
If any of 4–7 breaks: that's a defect in this product OR in Manifold v1.2's parameterized-leaf capability (and the existing harness specs at ../manifold/tests/browser/test_param_*.spec.js would catch it).
Cross-references
- Exploration note (the substrate for this whole project):
.planning/notes/qui-plugin-architecture-exploration.md - Manifold v1.2 deliverables this depends on:
../manifold/.planning/milestones/v1.2-ROADMAP.md - DOC-01 worked example (the doc qui-monitor must conform to):
../manifold/docs/PLUGIN-AUTHOR-GUIDE.md§Parameterized leaves - Card portability design (binding semantics, outcome names):
../manifold/docs/CARD-PORTABILITY.md - Test fixture this product is the production form of:
../manifold/tests/browser/fixtures/qui-test/ - Carve-out conventions:
../CARVE-OUT-NOTES.md - LXC 101 (qBit instances Karl runs):
../CLAUDE.mdLXC map - Qui upstream: https://github.com/autobrr/qui
Voice / cross-reference
Every doc in docs/ leads with WHY, names failure modes explicitly, provides recovery commands, and cross-links the next runbook. Plain English infra-admin voice — Karl's audience is an admin running services, not a dev shipping features. No leverage / comprehensive / seamless / robust / dive-in / delve. Runbooks read at 3 AM.
License
See LICENSE.