{% extends "base.html" %} {% block title %}{{ m.name }}{% endblock %} {% block content %}
{{ m.schedule_cron }} {{ m.schedule_tz }}{% else %}Expected every {{ m.period_minutes }} min{% endif %} · grace {{ m.grace_minutes }} min{% if m.max_duration_minutes %} · max run {{ m.max_duration_minutes }} min{% endif %} · {{ m.ping_count }} pings · last ping {{ m.last_ping_at|timeago }}{% if m.last_duration_ms %} · last run {{ fmt_duration(m.last_duration_ms // 1000) }}{% endif %}{{ m.selector }}{% endif %}{% if m.keyword %} · keyword {{ m.keyword }} {{ m.keyword_rule }}{% endif %}{% if m.mode == 'price' and m.price_rule != 'any' %} · alert when {{ m.price_rule }} {{ m.price_threshold }}{% endif %}{% if m.escalation_minutes %} · escalates after {{ m.escalation_minutes }} min{% endif %}{{ base_url }}/ping/{{ m.ping_token }}{{ base_url }}/ping/{{ m.ping_token }}/start{{ base_url }}/ping/{{ m.ping_token }}/fail
{{ base_url }}/ping/{{ m.ping_token }}/$?./job.sh 2>&1 | curl -fsS --data-binary @- {{ base_url }}/ping/{{ m.ping_token }}# crontab: start, run, report exit code with output
0 2 * * * curl -fsS -m 10 {{ base_url }}/ping/{{ m.ping_token }}/start >/dev/null; /usr/local/bin/backup.sh > /tmp/backup.log 2>&1; curl -fsS -m 10 --data-binary @/tmp/backup.log {{ base_url }}/ping/{{ m.ping_token }}/$? >/dev/null
# GitHub Actions / any CI
- run: curl -fsS {{ base_url }}/ping/{{ m.ping_token }}/start
- run: make deploy
- run: curl -fsS {{ base_url }}/ping/{{ m.ping_token }}
if: success()
- run: curl -fsS {{ base_url }}/ping/{{ m.ping_token }}/fail
if: failure()
# Python
import httpx; httpx.get("{{ base_url }}/ping/{{ m.ping_token }}/start")
...
httpx.post("{{ base_url }}/ping/{{ m.ping_token }}", content=log_text)| {% if r.ok %}pass{% else %}fail{% endif %} | {{ r.rule }} | {{ r.detail }} |
No evaluation recorded yet.
{% else %}No assertions configured. Add JSON, text or header assertions so a "200 OK" with a broken body still fails.
{% endif %}{{ checks[0].detail.description }}
{% for t in checks[0].detail.incidents or [] %}{{ latest_evidence.evidence.body }}| {{ t }} | {{ vals|join(' ')|safe }} |
No snapshot yet.
{% endif %}No changes detected yet. When the page changes you'll see exactly which lines were added and removed here.
{% elif m.mode == 'heartbeat' %}Job output posted with pings appears under the Checks & logs tab.
{% elif m.mode in ('uptime','api','tcp','flow','dependency','health') %}This monitor type doesn't diff content. Failure evidence (headers, body, timing) is captured on every failed check.
{% endif %} {% if m.last_text %}| {{ i.started_at|timeago }} | {{ i.kind }}{% if not i.resolved_at %} · open{% endif %} | {{ fmt_duration(i.duration_seconds) }} {{ (i.cause or '')[:100] }} |
No incidents. 🎉
{% endif %}| {{ al.created_at|timeago }} | {{ al.kind }} | {{ al.title }} {% for k, v in al.delivery.items() %}{{ k.split(':')[0] }}: {{ 'sent' if v == 'sent' else ('skipped' if k in ('muted','maintenance') else 'failed') }}{% if not loop.last %} · {% endif %}{% endfor %} |
No alerts yet.
{% endif %}Fires 20 requests, 5 at a time, and reports latency percentiles, errors and rate-limit (429) responses. Good for spotting rate limits, cold starts and flaky endpoints.
{% if m.last_burst %}{% set b = m.last_burst %}| When | Result | Detail | |
|---|---|---|---|
| {{ c.created_at|dt }} | {% if c.detail and c.detail.get('event') == 'start' %}start{% elif c.ok %}{{ c.status_code or 'ok' }}{% else %}{{ c.error_kind or 'fail' }}{% endif %}{% if c.changed %} changed{% endif %}{% if c.detail and c.detail.get('recovered_on_retry') %} retried{% endif %} | {% if c.location %}{{ c.location }} {% endif %}{% if c.ok %}{% if c.duration_ms is not none %}ran {{ fmt_duration(c.duration_ms // 1000) }}{% elif c.latency_ms is not none %}{{ c.latency_ms }} ms{% else %}ping{% endif %}{% if c.value is not none %} · value {{ c.value }}{% endif %}{% if c.detail and c.detail.get('exit_code') is not none %} · exit {{ c.detail.exit_code }}{% endif %}{% else %}{{ (c.error or '')[:110] }}{% endif %}{% if c.output %}log ({{ c.output|length }} chars){{ c.output[:3000] }} |
{% if c.diff or c.evidence or c.assertion_results or (c.detail and c.detail.get('steps')) or c.snapshot %}details{% endif %} |
| No checks yet. | |||
No incidents in the last 30 days.
{% endif %} {% if a.error_kinds %}| {{ al.created_at|dt }} | {{ al.kind }} | {{ al.title }} {{ al.body[:300] }} {% for k, v in al.delivery.items() %}{{ k.split(':')[0] }}: {{ v }}{% if not loop.last %} · {% endif %}{% endfor %} |
No alerts yet.
{% endif %}No run recorded yet.
{% endif %}| {{ c.status }} | {{ c.name }} {{ c.message }} |
Nothing received yet. Make sure the endpoint returns JSON and, if you set a secret, checks the vigil-health-secret header.
Secret header sent with every request:
vigil-health-secret: {{ m.health_secret or '(none)' }}
{% if user.can_edit %}{% endif %}
Return any of these shapes:
{"checkResults": [
{"name": "Database", "status": "ok"},
{"name": "Queue", "status": "failed", "notificationMessage": "1,204 jobs waiting"}
]}
// or Laravel Health: {"status":"ok","checks":{"db":{"status":"ok"}}}
// or Spring Actuator: {"status":"UP","components":{...}}
// or simply: {"status": "ok"}
Each check gets its own alert when it fails and when it recovers. Any failed check marks the monitor down; warning keeps it up but shows here.
Registrars send renewal emails that get lost. "clientTransferProhibited" is good: it means the domain is locked against hijacking.
| {{ t }} | {{ vals|join(' ')|safe }} |
No snapshot yet.
{% endif %}No scan yet. Scans check HSTS, CSP, X-Frame-Options, cookie flags, TLS 1.0–1.3 support and leaked server versions.
{% endif %}Nothing to fix. 🎉
{% else %}Run a scan to see findings and how to fix each one.
{% endif %}