The 2026-06-16/17 observability update externalised metrics to
Prometheus + node_exporter + Grafana, but the legacy Manage-side
SSH-scraping monitor was never removed. It duplicated the new stack,
ran SSH df on every machine every 300s, and fed nothing (its UI was
deleted in e2ad731). This slice decommissions the duplication.
Removed (backend):
- services/monitoring_poller.py (MonitoringPoller) — entire file
- services/monitoring_actions.py (disk_space, run_machine_operation,
poll_machine_snapshot, build_machine_client) — entire file;
run_machine_operation had only 2 callers (the poller + /disk), both gone
- tests/test_monitoring_actions.py
- endpoints: POST /api/monitoring/poller, GET /machines/{id}/actions,
GET /disk (and the now-dead _resolve_machine helper)
- lifespan wiring (main.py), dependency wrapper (dependencies.py),
poller.start()/kick() from machine save (routers/settings.py)
- SettingsStore: monitoring_machine_actions table CREATE + 2 indexes +
record/list/prune_machine_actions methods; DROP TABLE IF EXISTS on
startup cleans existing DBs (user-approved)
- config knobs: monitoring_poll_interval_seconds,
monitoring_poll_initial_delay_seconds, monitoring_action_retention_days
- test_api.py: TestMonitoring._ensure_machine + test_disk
Kept (fits the new model): /machines, /prometheus-targets, /alerts,
/alertmanager-status, /alertmanager-webhook; the disk_usage JOB template
(manual on-demand, not monitoring); node_exporter_* machine fields
(they point Prometheus at the right host).
Gate: backend pytest 173 passed; ruff clean.
9.4 KiB
Plan — decommission-monitoring-poller
Status: DRAFT — awaiting user approval before implementation. Scope: a focused backend+frontend decommission, not a full SDD change. Plan-then-implement (user-approved 2026-06-17). Root cause this addresses: the 2026-06-16/17 observability update externalised metrics to Prometheus+Grafana+Loki+Alertmanager, but the legacy Manage-side SSH-scraping monitor (the
MonitoringPoller,/monitoring/disk,/monitoring/machines/{id}/actions, and themonitoring_machine_actionsSQLite table) was never removed. It duplicates the new stack, drains SSH budget every 300s, and feeds nothing (its UI was deleted ine2ad731).
1. Problem
Manage runs a background thread (MonitoringPoller) that, every 300s, SSHes into every configured machine, runs df, and stores the result in its own SQLite table (monitoring_machine_actions, 30-day retention). After the observability update, Prometheus already scrapes node_exporter on these machines and Grafana already owns the dashboards. The poller is pure duplication: more SSH sessions, more stale state, a second source of truth for "disk usage," and a SQLite table that nothing reads.
The alerting side (Alertmanager proxy + /alerts + /alertmanager-status + /alertmanager-webhook + /prometheus-targets + /machines) already fits the new model and is untouched by this change.
2. Goals / non-goals
Goals
- Stop the duplicated SSH-scraping of system metrics.
- Remove the dead
/disk,/poller,/machines/{id}/actionssurface and the SQLite history that fed it. - Remove the now-orphaned frontend
DiskSpaceCard+DiskSpacetype. - Leave Manage a clean thin-dashboard: Alertmanager alerts + Prometheus target health + Grafana deep-links.
Non-goals
- Do NOT touch the Alertmanager proxy,
/prometheus-targets,/machines, or/alertmanager-webhook— they fit the model. - Do NOT remove the
disk_usagejob template injobs.py(user-approved: it is a manual on-demand Actions job, not monitoring). - Do NOT remove
node_exporter_*fields onMonitoringMachine— they configure where Prometheus scrapes; that is correct and stays. - Do NOT introduce a Prometheus query proxy / PromQL reader in this change (that was the alternative the user did not pick).
- Do NOT add new features. This is a removal.
3. Exact removal map (verified against source)
Backend — delete entirely
backend/src/media_library_viewer_api/services/monitoring_poller.py(theMonitoringPollerclass,PollerConfig,_MONITORING_POLLER,get_monitoring_poller).- Verified sole callers:
main.pylifespan,dependencies.pywrapper,routers/monitoring.py:/poller,routers/settings.py(machine save →poller.start()/kick()).
- Verified sole callers:
backend/src/media_library_viewer_api/services/monitoring_actions.py(the whole file:build_machine_client,disk_space,summarize_operation_result,json_compact,run_machine_operation,poll_machine_snapshot).- Verified:
run_machine_operationhas exactly 2 callers (poll_machine_snapshothere, and/monitoring/disk) — both going.tasks.pydoes NOT use it. Nothing else imports the module.
- Verified:
backend/tests/test_monitoring_actions.py(36 lines, testspoll_machine_snapshot).
Backend — edit in place
backend/src/media_library_viewer_api/main.pylifespan (lines ~48–56): removemonitoring_poller = get_monitoring_poller(),monitoring_poller.start(),monitoring_poller.stop(), and theget_monitoring_pollerimport on line 16. Keepbackup_pollerandmail_queueintact.backend/src/media_library_viewer_api/dependencies.py: remove theMonitoringPollerimport block (lines 24–29) and theget_monitoring_pollerwrapper (lines 254–256).backend/src/media_library_viewer_api/routers/monitoring.py: remove imports ofdisk_space,run_machine_operation,poll_machine_snapshot; remove the three endpoints/poller(99),/machines/{machine_id}/actions(113),/disk(127). Keep/machines,/prometheus-targets,/alerts,/alertmanager-status,/alertmanager-webhook. Also drop the now-unused_resolve_machinehelper if it becomes unreferenced after/diskand/actionsremoval (verify during impl —/machinesdoes not use it).backend/src/media_library_viewer_api/routers/settings.py(lines 198–204 and 218–224): remove theget_monitoring_poller()+poller.start()+poller.kick()calls frompost_machineandput_machine. Keepwrite_prometheus_targets(store)(that is the new-model target generation).backend/src/media_library_viewer_api/services/settings_store.py:- Remove
CREATE TABLE IF NOT EXISTS monitoring_machine_actions(lines ~90) and its two indexes (idx_monitoring_machine_actions_machine_time,idx_monitoring_machine_actions_action_status, lines ~183–190) frominit_schema. - Remove methods
record_machine_action(566),list_machine_actions(610),prune_machine_actions(638). - Note: existing databases will keep the orphaned
monitoring_machine_actionstable harmlessly (no migration framework here —init_schemaisCREATE TABLE IF NOT EXISTS+ ad-hocALTER). A one-lineDROP TABLE IF EXISTScan be added toinit_schemafor cleanliness; decide at impl time.
- Remove
backend/src/media_library_viewer_api/config.py: removemonitoring_poll_interval_seconds(56),monitoring_poll_initial_delay_seconds(57),monitoring_action_retention_days(58).
Backend — tests to fix
backend/tests/test_api.py:TestMonitoring.test_disk(line 615) — remove (tests/api/monitoring/disk).TestMonitoring.test_prometheus_targets_emptyand..._returns_enabled_ssh_node_exporter— keep (test the surviving/prometheus-targets).TestSettingsMachines— keep but verify they still pass after thepollercalls are removed frompost/put_machine.TestAlertmanager— keep (untouched).- The
disk_usagereference at line 576/586 is the Jobs test (/api/jobs/run), NOT the monitoring poller — keep (the job template stays).
Frontend — delete
frontend/src/components/DiskSpaceCard.tsx— verified orphaned (only__tests__/DiskSpaceCard.test.tsximports it; no page uses it).frontend/src/components/__tests__/DiskSpaceCard.test.tsx.frontend/src/types/index.tsDiskSpaceinterface (line 279) — remove after confirming no importer (grep shows none outside the type file).
Docs
AGENTS.mdline 25 ("starts the mail queue and monitoring poller") → "...starts the mail queue and backup alert poller."docs/monitoring-logging-design.mdline 65 (describes the poller) → update or strike the poller paragraph.docs/MIGRATION_PLAN.mdline 110 (/api/monitoring/diskrow) → remove the row.docs/REQUIREMENTS.md→ add a note that Manage-side system-metric scraping is retired in favour of the external observability stack.docs/superpowers/specs/2026-05-11-backup-monitoring-design.mdis a historical spec; leave as-is (it is an archived design doc).
4. Slice plan (≤400 lines each, build+pytest green per slice)
- Slice 1 — Backend removal (endpoints + poller + actions + store + config). Delete
monitoring_poller.py,monitoring_actions.py,test_monitoring_actions.py; editmain.py,dependencies.py,routers/monitoring.py,routers/settings.py,settings_store.py,config.py; fixtest_api.py(test_diskremoved,TestSettingsMachinesre-checked). Gate:cd backend && PYTHONPATH=src pytest. - Slice 2 — Frontend orphan removal. Delete
DiskSpaceCard.tsx+ its test +DiskSpacetype. Gate:cd frontend && npm run build && npm run lint && npm test. - Slice 3 — Docs.
AGENTS.md,docs/monitoring-logging-design.md,docs/MIGRATION_PLAN.md,docs/REQUIREMENTS.md. Gate: none (docs); commit standalone.
Estimated total: ~500–700 lines deleted, ~50–100 added (edits). Each slice well under 400.
5. Risks & verification
- Hidden caller of
run_machine_operation/poll_machine_snapshot: mitigated — grep shows exactly the callers listed; re-grep at slice-1 start. TestSettingsMachinesbreakage oncepoller.start()/kick()is removed frompost/put_machine: those tests mockwrite_prometheus_targetsand don't assert on the poller; should pass. If they referenceget_monitoring_poller, fix by dropping the assertion.- Orphaned SQLite table on existing DBs: harmless (empty, unused). Optional
DROP TABLE IF EXISTS monitoring_machine_actionsininit_schemafor cleanliness. - No browser smoke: same caveat as the UI rework; backend covered by pytest.
_resolve_machinein monitoring.py may become unused after/disk+/actionsremoval; remove if so.
6. Acceptance
cd backend && PYTHONPATH=src pytestgreen (withtest_disk+test_monitoring_actions.pyremoved).grep -rnE 'MonitoringPoller|poll_machine_snapshot|/monitoring/disk|monitoring_machine_actions|monitoring_poll_interval_seconds|DiskSpaceCard' backend/ frontend/src/→ only historical/docs hits (spec.md archive is fine).cd frontend && npm run build && npm run lint && npm testgreen.- Docs updated to reflect Manage no longer scrapes its own metrics.
7. Open questions for the user (none blocking, defaults shown)
- Q1. Existing DBs' orphaned
monitoring_machine_actionstable — (a) addDROP TABLE IF EXISTStoinit_schemafor a clean slate [default], or (b) leave it harmless? - Q2. Commit/PR mechanics — same as the UI rework (commit per slice, no push until you say)?