fix(prometheus, syslog): a target or pattern pauses with a Pause/Resume button, not an "on" tick box in its form (#6419)
CI / lint (push) Successful in 2s
CI / unit (push) Failing after 1m42s
CI / integration (push) Successful in 1m45s
CI / publish (push) Skipped

Monitors, schedules and maintenance plans already pause with a
Pause/Resume button and say "Paused" where they're listed. Prometheus
targets and syslog patterns only had a tick box inside the edit form,
so a target refused by its endpoint couldn't be stopped from its page.

- Both get a Pause/Resume button (target page and list; patterns list)
  posting to /toggle, recorded in the audit log, and a Paused badge.
- The forms lose the tick box. A new one starts running; an edit keeps
  whether it is paused.
- A target's "off" state is now "paused", in its pages and MCP tools.
  A resumed target reads "not scraped yet" until its next scrape, and its
  counter rates start over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 17:48:31 -04:00
co-authored by Claude Opus 5.5
parent 0b787a0676
commit d20d78498e
15 changed files with 104 additions and 51 deletions
+9 -2
View File
@@ -11,6 +11,7 @@ from tests.test_style_tokens import hex_colours
def _view(name, reach, says="Answering.", ok_minutes=None, capped=0):
now = datetime.now(timezone.utc)
return {"id": f"id-{name}", "name": name, "url": f"http://{name}:8081/metrics", "reach": reach,
"enabled": reach != "paused",
"says": says, "scraped_at": now - timedelta(seconds=20),
"ok_at": now - timedelta(minutes=ok_minutes) if ok_minutes is not None else None,
"interval": 60, "kept": 120, "dropped": 30, "capped": capped, "cap": 1000,
@@ -20,7 +21,8 @@ def _view(name, reach, says="Answering.", ok_minutes=None, capped=0):
async def test_a_target_not_answering_says_why_and_the_cap_is_shown(app):
from quart import session
targets = [_view("archives", "down", "Answered 401: the token was refused.", ok_minutes=180),
_view("busy", "up", capped=37), _view("new", "pending")]
_view("busy", "up", capped=37), _view("new", "pending"),
_view("resting", "paused", "Paused: not scraped until it's resumed.")]
async with app.test_request_context("/plugins/prometheus/"):
session["user_role"] = "admin"
page = await render_plugin_template(app, "prometheus", "prometheus/index.html", targets=targets)
@@ -30,6 +32,10 @@ async def test_a_target_not_answering_says_why_and_the_cap_is_shown(app):
assert "37 series weren't stored" in page and "the cap of 1000" in page
assert '<span class="stat-tile-value">42 ms</span>' in page
assert "the first scrape runs within seconds of saving" in page
# Paused and resumed from the list (#6419), each row saying which it is.
assert '<span class="badge badge-dim">Paused</span>' in page and "not scraped until it's resumed" in page
assert 'action="/plugins/prometheus/targets/id-resting/toggle"' in page
assert page.count(">Pause</button>") == 3 and page.count(">Resume</button>") == 1
assert 'href="/plugins/prometheus/targets/new"' in page and 'href="/plugins/prometheus/targets/id-busy/edit"' in page
main = page[page.index("<main"):]
assert not hex_colours(main), hex_colours(main)
@@ -43,7 +49,7 @@ async def test_a_viewer_sees_no_editing_and_an_empty_page_offers_to_add(app):
targets=[_view("archives", "up")])
session["user_role"] = "admin"
empty = await render_plugin_template(app, "prometheus", "prometheus/index.html", targets=[])
assert "/edit" not in page and "targets/new" not in page
assert "/edit" not in page and "targets/new" not in page and "/toggle" not in page
assert "No targets yet." in empty and 'href="/plugins/prometheus/targets/new"' in empty
@@ -103,3 +109,4 @@ async def test_the_form_never_echoes_the_token_and_offers_test(app):
assert 'name="token" type="password"' in page and "kept; type to change" in page
assert 'name="clear_token"' in page and 'hx-post="/plugins/prometheus/targets/test"' in page
assert "fabledarchives_*" in page and "Delete target" in page
assert 'name="enabled"' not in page # paused from the target's page, not here (#6419)