From 1a92a85955d8772e00634ccbe8048d1f0dd4b6fd Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Mon, 12 Oct 2026 11:31:12 +0100 Subject: [PATCH 1/4] Monitor Healthchecks.io checks in SquaredUp Healthchecks.io is a cron job and heartbeat monitoring service with no SquaredUp integration. This adds a low-code plugin built on its Management API v3, so check status, up/down history and ping logs can sit alongside other monitoring data. A configurable server URL also covers self-hosted Healthchecks instances. API keys are scoped to one project, so each data source is one project. Both key types are supported and read-only is recommended: the plugin never writes, and a read-only key covers everything except the ping log. Read-only keys identify checks by `unique_key` instead of `uuid`; checks are indexed by whichever is present, since both are accepted by the check and flips endpoints. The ping log route only matches UUIDs, so a read-only key gets an HTML 404 rather than a 401; the pings error handler detects a `unique_key` scope and explains that a read-write key is needed. Checks are the only first-class entity in the API. The `checks` stream lists them in one request and serves import, config validation, the overview and the Check perspective through optional check and tag pickers. Flips and pings only exist per check, so `statusHistory` and `pings` are scoped one request per check, matching how Pingdom and other per-object endpoints are modelled. Status history turns flips into up/down periods clipped to the timeframe, so one stream gives both an availability split and an outage list. Tags are imported as their own objects and related to checks with a `has tag` correlation rule. The check's tags use the array shape so a single column drives display, filtering, the multi-value `checkTags` graph property and the `objectPropertyPath` read-back in the per-check streams. `tags` is a reserved vertex property, hence `checkTags`. Job duration is only recorded for pings that follow a `/start` signal, which plain cron setups rarely send. The Check perspective therefore charts ping results per day rather than duration, and duration stays in the recent pings table. Co-Authored-By: Claude Opus 5.5 --- .../Healthchecks.io/v1/configValidation.json | 13 + .../correlationRules/relate-check-to-tag.json | 20 + plugins/Healthchecks.io/v1/cspell.json | 3 + plugins/Healthchecks.io/v1/custom_types.json | 16 + .../v1/dataStreams/checks.json | 177 ++++++++ .../Healthchecks.io/v1/dataStreams/pings.json | 138 ++++++ .../v1/dataStreams/scripts/checks.js | 31 ++ .../scripts/errorHandling/pings.js | 19 + .../v1/dataStreams/scripts/pings.js | 20 + .../v1/dataStreams/scripts/statusHistory.js | 56 +++ .../v1/dataStreams/scripts/tags.js | 6 + .../v1/dataStreams/statusHistory.json | 103 +++++ .../Healthchecks.io/v1/dataStreams/tags.json | 28 ++ .../v1/defaultContent/check.dash.json | 423 ++++++++++++++++++ .../v1/defaultContent/manifest.json | 16 + .../v1/defaultContent/overview.dash.json | 358 +++++++++++++++ .../v1/defaultContent/scopes.json | 32 ++ .../v1/defaultContent/tag.dash.json | 415 +++++++++++++++++ plugins/Healthchecks.io/v1/docs/README.md | 61 +++ plugins/Healthchecks.io/v1/icon.svg | 9 + .../v1/indexDefinitions/default.json | 50 +++ plugins/Healthchecks.io/v1/metadata.json | 59 +++ plugins/Healthchecks.io/v1/ui.json | 34 ++ 23 files changed, 2087 insertions(+) create mode 100644 plugins/Healthchecks.io/v1/configValidation.json create mode 100644 plugins/Healthchecks.io/v1/correlationRules/relate-check-to-tag.json create mode 100644 plugins/Healthchecks.io/v1/cspell.json create mode 100644 plugins/Healthchecks.io/v1/custom_types.json create mode 100644 plugins/Healthchecks.io/v1/dataStreams/checks.json create mode 100644 plugins/Healthchecks.io/v1/dataStreams/pings.json create mode 100644 plugins/Healthchecks.io/v1/dataStreams/scripts/checks.js create mode 100644 plugins/Healthchecks.io/v1/dataStreams/scripts/errorHandling/pings.js create mode 100644 plugins/Healthchecks.io/v1/dataStreams/scripts/pings.js create mode 100644 plugins/Healthchecks.io/v1/dataStreams/scripts/statusHistory.js create mode 100644 plugins/Healthchecks.io/v1/dataStreams/scripts/tags.js create mode 100644 plugins/Healthchecks.io/v1/dataStreams/statusHistory.json create mode 100644 plugins/Healthchecks.io/v1/dataStreams/tags.json create mode 100644 plugins/Healthchecks.io/v1/defaultContent/check.dash.json create mode 100644 plugins/Healthchecks.io/v1/defaultContent/manifest.json create mode 100644 plugins/Healthchecks.io/v1/defaultContent/overview.dash.json create mode 100644 plugins/Healthchecks.io/v1/defaultContent/scopes.json create mode 100644 plugins/Healthchecks.io/v1/defaultContent/tag.dash.json create mode 100644 plugins/Healthchecks.io/v1/docs/README.md create mode 100644 plugins/Healthchecks.io/v1/icon.svg create mode 100644 plugins/Healthchecks.io/v1/indexDefinitions/default.json create mode 100644 plugins/Healthchecks.io/v1/metadata.json create mode 100644 plugins/Healthchecks.io/v1/ui.json diff --git a/plugins/Healthchecks.io/v1/configValidation.json b/plugins/Healthchecks.io/v1/configValidation.json new file mode 100644 index 00000000..ce94eb1c --- /dev/null +++ b/plugins/Healthchecks.io/v1/configValidation.json @@ -0,0 +1,13 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { + "name": "checks" + }, + "required": true, + "error": "Could not connect. Check the API key is a project API key (not a ping key) and the server URL is correct.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/Healthchecks.io/v1/correlationRules/relate-check-to-tag.json b/plugins/Healthchecks.io/v1/correlationRules/relate-check-to-tag.json new file mode 100644 index 00000000..8b2dc4d5 --- /dev/null +++ b/plugins/Healthchecks.io/v1/correlationRules/relate-check-to-tag.json @@ -0,0 +1,20 @@ +{ + "displayName": "Check has Tag", + "source": { + "types": "Check" + }, + "target": { + "types": "Tag" + }, + "conditions": [ + { + "sourceProperty": "checkTags", + "operator": "equals", + "targetProperty": "name" + } + ], + "labels": { + "forward": "has tag", + "reverse": "tag of" + } +} diff --git a/plugins/Healthchecks.io/v1/cspell.json b/plugins/Healthchecks.io/v1/cspell.json new file mode 100644 index 00000000..d3d5d45a --- /dev/null +++ b/plugins/Healthchecks.io/v1/cspell.json @@ -0,0 +1,3 @@ +{ + "words": ["healthchecks", "OnCalendar", "systemd"] +} diff --git a/plugins/Healthchecks.io/v1/custom_types.json b/plugins/Healthchecks.io/v1/custom_types.json new file mode 100644 index 00000000..f011091d --- /dev/null +++ b/plugins/Healthchecks.io/v1/custom_types.json @@ -0,0 +1,16 @@ +[ + { + "name": "Check", + "sourceType": "Check", + "icon": "heart-pulse", + "singular": "Check", + "plural": "Checks" + }, + { + "name": "Tag", + "sourceType": "Tag", + "icon": "tag", + "singular": "Tag", + "plural": "Tags" + } +] diff --git a/plugins/Healthchecks.io/v1/dataStreams/checks.json b/plugins/Healthchecks.io/v1/dataStreams/checks.json new file mode 100644 index 00000000..cd5132fb --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/checks.json @@ -0,0 +1,177 @@ +{ + "name": "checks", + "displayName": "Checks", + "description": "Current status, schedule and last ping of each check in the project", + "tags": ["Monitoring", "Status"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "checks/", + "getArgs": [ + { + "key": "tag", + "value": "{{ (typeof tag !== 'undefined' && tag && tag.length) ? tag.flatMap((o) => o.rawId) : null }}" + } + ], + "paging": { + "mode": "none" + }, + "postRequestScript": "checks.js" + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "checks", + "label": "Filter by check", + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Check"] + } + } + }, + { + "type": "objects", + "name": "tag", + "label": "Filter by tag", + "help": "Returns only checks that have every selected tag.", + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Tag"] + } + } + } + ], + "metadata": [ + { + "name": "id", + "displayName": "Check ID", + "shape": "string", + "role": "id", + "visible": false + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": ["up"], + "warning": ["grace"], + "error": ["down"], + "unmonitored": ["paused"], + "unknown": ["new"] + } + } + ] + }, + { + "name": "started", + "displayName": "Running", + "shape": "boolean" + }, + { + "name": "last_ping", + "displayName": "Last ping", + "shape": "date", + "role": "timestamp" + }, + { + "name": "next_ping", + "displayName": "Next ping due", + "shape": "date" + }, + { + "name": "secondsSinceLastPing", + "displayName": "Time since last ping", + "computed": true, + "valueExpression": "{{ $['last_ping'] ? Math.max(0, Math.round((Date.now() - Date.parse($['last_ping'])) / 1000)) : null }}", + "shape": "seconds" + }, + { + "name": "kind", + "displayName": "Kind", + "shape": "string" + }, + { + "name": "timeout", + "displayName": "Period", + "shape": "seconds" + }, + { + "name": "schedule", + "displayName": "Schedule", + "shape": "string" + }, + { + "name": "tz", + "displayName": "Time zone", + "shape": "string" + }, + { + "name": "grace", + "displayName": "Grace time", + "shape": "seconds" + }, + { + "name": "tags", + "displayName": "Tags", + "shape": ["array", { "itemShape": "string" }] + }, + { + "name": "slug", + "displayName": "Slug", + "shape": "string" + }, + { + "name": "desc", + "displayName": "Description", + "shape": "string", + "role": "description" + }, + { + "name": "n_pings", + "displayName": "Total pings", + "shape": "number" + }, + { + "name": "manual_resume", + "displayName": "Manual resume", + "shape": "boolean", + "visible": false + }, + { + "name": "ping_url", + "displayName": "Ping URL", + "shape": "url", + "visible": false + }, + { + "name": "update_url", + "displayName": "API URL", + "visible": false + }, + { + "name": "checkUrl", + "displayName": "Link", + "computed": true, + "valueExpression": "{{ $['update_url'] ? $['update_url'].replace(/[/]api[/]v[0-9]+[/]checks[/]/, '/checks/') + '/details/' : null }}", + "shape": ["url", { "label": "Open in Healthchecks.io" }] + }, + { + "sourceId": "id", + "sourceType": "Check", + "name": "name" + } + ], + "timeframes": false +} diff --git a/plugins/Healthchecks.io/v1/dataStreams/pings.json b/plugins/Healthchecks.io/v1/dataStreams/pings.json new file mode 100644 index 00000000..51706eb5 --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/pings.json @@ -0,0 +1,138 @@ +{ + "name": "pings", + "displayName": "Pings", + "description": "Pings received by a check, including job run times (requires a read-write API key)", + "tags": ["Monitoring", "Events"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "checks/{{object.rawId}}/pings/", + "paging": { + "mode": "none" + }, + "postRequestScript": "pings.js", + "errorHandling": { + "type": "script", + "script": "errorHandling/pings.js" + } + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Check"] + } + }, + "metadata": [ + { + "name": "checkId", + "displayName": "Check ID", + "shape": "string", + "visible": false + }, + { + "name": "checkName", + "displayName": "Check", + "shape": "string", + "role": "label" + }, + { + "name": "checkTags", + "displayName": "Tags", + "sourceId": "checkId", + "sourceType": "Check", + "objectPropertyPath": "checkTags", + "shape": ["array", { "itemShape": "string" }] + }, + { + "name": "date", + "displayName": "Date", + "shape": "date", + "role": "timestamp" + }, + { + "name": "type", + "displayName": "Type", + "shape": "string" + }, + { + "name": "result", + "displayName": "Result", + "computed": true, + "valueExpression": "{{ $['type'] }}", + "shape": [ + "state", + { + "map": { + "success": ["success"], + "error": ["fail"], + "unknown": ["start", "log", "ign"] + } + } + ] + }, + { + "name": "duration", + "displayName": "Duration", + "shape": "seconds" + }, + { + "name": "exitstatus", + "displayName": "Exit status", + "shape": "number" + }, + { + "name": "method", + "displayName": "Method", + "shape": "string" + }, + { + "name": "scheme", + "displayName": "Scheme", + "shape": "string" + }, + { + "name": "remote_addr", + "displayName": "Source IP", + "shape": "string" + }, + { + "name": "ua", + "displayName": "User agent", + "shape": "string" + }, + { + "name": "rid", + "displayName": "Run ID", + "shape": "string", + "visible": false + }, + { + "name": "n", + "displayName": "Ping number", + "shape": "number", + "visible": false + }, + { + "name": "body_url", + "displayName": "Body URL", + "visible": false + }, + { + "sourceId": "checkId", + "sourceType": "Check", + "name": "checkName" + } + ], + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days" + ], + "defaultShaping": { + "sort": { + "by": [["date", "desc"]] + } + } +} diff --git a/plugins/Healthchecks.io/v1/dataStreams/scripts/checks.js b/plugins/Healthchecks.io/v1/dataStreams/scripts/checks.js new file mode 100644 index 00000000..7d25de89 --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/scripts/checks.js @@ -0,0 +1,31 @@ +// Read-write keys return `uuid`; read-only keys omit it and return `unique_key` +// instead. Both are accepted by the per-check endpoints, so either is a stable id. +const idOf = (c) => c.uuid || c.unique_key; + +// Optional `checks` object picker (or a bound dashboard variable). Empty or +// absent means every check in the project. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.checks) || []; +const selectedIds = new Set( + selected.map((o) => unwrap(o.rawId)).filter(Boolean), +); + +// A schedule is either a cron expression (five space-separated fields) or a +// systemd OnCalendar expression; checks without one use a simple period. +const kindOf = (schedule) => { + if (!schedule) { + return "Simple"; + } + return schedule.trim().split(/\s+/).length === 5 ? "Cron" : "OnCalendar"; +}; + +result = ((data && data.checks) || []) + .filter((c) => !selectedIds.size || selectedIds.has(idOf(c))) + .map((c) => ({ + ...c, + id: idOf(c), + name: c.name || c.slug || idOf(c), + kind: kindOf(c.schedule), + // The API returns tags as one space-separated string. + tags: (c.tags || "").split(/\s+/).filter(Boolean), + })); diff --git a/plugins/Healthchecks.io/v1/dataStreams/scripts/errorHandling/pings.js b/plugins/Healthchecks.io/v1/dataStreams/scripts/errorHandling/pings.js new file mode 100644 index 00000000..af9263a2 --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/scripts/errorHandling/pings.js @@ -0,0 +1,19 @@ +// Healthchecks.io returns API errors as {"error": "..."}, but a URL that matches +// no route returns an HTML 404 page instead. +const apiMsg = data && typeof data === "object" && data.error; +const readOnlyMsg = + "The ping log needs a Healthchecks.io read-write API key (read-only keys cannot read pings)"; + +// Read-only keys identify checks by a 40-character `unique_key`. The ping log +// route only accepts UUIDs, so those requests 404 rather than returning 401. +const object = context && context.objects && context.objects[0]; +const rawId = object && (Array.isArray(object.rawId) ? object.rawId[0] : object.rawId); +const isReadOnlyId = typeof rawId === "string" && /^[0-9a-f]{40}$/i.test(rawId); + +if (isReadOnlyId || response.status === 401 || response.status === 403) { + result = readOnlyMsg + (apiMsg ? ": " + apiMsg : ""); +} else if (response.status === 404) { + result = "Check not found in Healthchecks.io (it may have been deleted)"; +} else { + result = apiMsg || "Request failed with HTTP " + response.status; +} diff --git a/plugins/Healthchecks.io/v1/dataStreams/scripts/pings.js b/plugins/Healthchecks.io/v1/dataStreams/scripts/pings.js new file mode 100644 index 00000000..cc9f2c1e --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/scripts/pings.js @@ -0,0 +1,20 @@ +// The ping log endpoint has no time filter, so keep only the pings inside the +// tile timeframe here. It returns the newest pings first, capped by plan. +const check = context.objects[0]; +const winStart = Date.parse(context.timeframe.start); +const winEnd = Date.parse(context.timeframe.end); + +result = ((data && data.pings) || []) + .filter((p) => { + const t = Date.parse(p.date); + return !Number.isNaN(t) && t >= winStart && t <= winEnd; + }) + .map((p) => ({ + ...p, + // Only present on some pings; emitted always so the columns exist for + // checks that never send start signals or exit codes. + duration: p.duration ?? null, + exitstatus: p.exitstatus ?? null, + checkId: check.rawId, + checkName: check.name, + })); diff --git a/plugins/Healthchecks.io/v1/dataStreams/scripts/statusHistory.js b/plugins/Healthchecks.io/v1/dataStreams/scripts/statusHistory.js new file mode 100644 index 00000000..28ba18f8 --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/scripts/statusHistory.js @@ -0,0 +1,56 @@ +// Turns the flip events (points in time) into up/down periods clipped to the +// timeframe window. The request deliberately has no `start`, so the last flip +// before the window is available to establish the state at the window start. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const check = context.objects[0]; +const checkId = check.rawId; +const checkName = check.name; +const indexedStatus = unwrap(check.status); + +const now = Date.now(); +const winStart = Date.parse(context.timeframe.start); +const rawEnd = Date.parse(context.timeframe.end); +// A period never extends beyond "now". +const winEnd = Math.min(rawEnd, now); +const reachesNow = rawEnd >= now - 60 * 1000; + +// The API wraps the list as { flips: [...] }; tolerate a bare array too. +const flipList = Array.isArray(data) ? data : (data && data.flips) || []; +const flips = flipList + .map((f) => ({ t: Date.parse(f.timestamp), up: f.up ? 1 : 0 })) + .filter((f) => !Number.isNaN(f.t)) + .sort((a, b) => a.t - b.t); + +const rows = []; +const emit = (from, to, up, isLast) => { + const s = Math.max(from, winStart); + const e = Math.min(to, winEnd); + if (e <= s) { + return; + } + rows.push({ + checkId, + checkName, + status: up ? "up" : "down", + start: new Date(s).toISOString(), + end: new Date(e).toISOString(), + durationSeconds: Math.round((e - s) / 1000), + ongoing: Boolean(isLast && reachesNow), + }); +}; + +if (flips.length === 0) { + // Stable for the whole retention period: use the indexed current state. + if (indexedStatus === "success" || indexedStatus === "warning") { + emit(winStart, winEnd, true, true); + } else if (indexedStatus === "error") { + emit(winStart, winEnd, false, true); + } +} else { + flips.forEach((f, i) => { + const isLast = i === flips.length - 1; + emit(f.t, isLast ? winEnd : flips[i + 1].t, f.up, isLast); + }); +} + +result = rows; diff --git a/plugins/Healthchecks.io/v1/dataStreams/scripts/tags.js b/plugins/Healthchecks.io/v1/dataStreams/scripts/tags.js new file mode 100644 index 00000000..c3f09545 --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/scripts/tags.js @@ -0,0 +1,6 @@ +// The badges endpoint is keyed by tag name; `*` is a pseudo-tag covering the +// whole project, not a real tag. +result = Object.keys((data && data.badges) || {}) + .filter((tag) => tag !== "*") + .sort() + .map((tag) => ({ name: tag })); diff --git a/plugins/Healthchecks.io/v1/dataStreams/statusHistory.json b/plugins/Healthchecks.io/v1/dataStreams/statusHistory.json new file mode 100644 index 00000000..ffdb22d0 --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/statusHistory.json @@ -0,0 +1,103 @@ +{ + "name": "statusHistory", + "displayName": "Status History", + "description": "Up and down periods of a check, derived from its status changes", + "tags": ["Monitoring", "Status"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "checks/{{object.rawId}}/flips/", + "getArgs": [ + { + "key": "end", + "value": "{{ Math.floor(timeframe.unixEnd) }}" + } + ], + "paging": { + "mode": "none" + }, + "postRequestScript": "statusHistory.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Check"] + } + }, + "metadata": [ + { + "name": "checkId", + "displayName": "Check ID", + "shape": "string", + "visible": false + }, + { + "name": "checkName", + "displayName": "Check", + "shape": "string", + "role": "label" + }, + { + "name": "checkTags", + "displayName": "Tags", + "sourceId": "checkId", + "sourceType": "Check", + "objectPropertyPath": "checkTags", + "shape": ["array", { "itemShape": "string" }] + }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": ["up"], + "error": ["down"] + } + } + ] + }, + { + "name": "start", + "displayName": "Start", + "shape": "date", + "role": "timestamp" + }, + { + "name": "end", + "displayName": "End", + "shape": "date" + }, + { + "name": "durationSeconds", + "displayName": "Duration", + "shape": "seconds" + }, + { + "name": "ongoing", + "displayName": "Current", + "shape": "boolean" + }, + { + "sourceId": "checkId", + "sourceType": "Check", + "name": "checkName" + } + ], + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "lastMonth", + "thisQuarter" + ], + "defaultShaping": { + "sort": { + "by": [["start", "desc"]] + } + } +} diff --git a/plugins/Healthchecks.io/v1/dataStreams/tags.json b/plugins/Healthchecks.io/v1/dataStreams/tags.json new file mode 100644 index 00000000..0a98dac1 --- /dev/null +++ b/plugins/Healthchecks.io/v1/dataStreams/tags.json @@ -0,0 +1,28 @@ +{ + "name": "tags", + "displayName": "Tags", + "description": "Tags used by checks in the project", + "tags": ["Monitoring"], + "baseDataSourceName": "httpRequestUnscoped", + "visibility": { + "type": "hidden" + }, + "config": { + "httpMethod": "get", + "endpointPath": "badges/", + "paging": { + "mode": "none" + }, + "postRequestScript": "tags.js" + }, + "matches": "none", + "metadata": [ + { + "name": "name", + "displayName": "Tag", + "shape": "string", + "role": "label" + } + ], + "timeframes": false +} diff --git a/plugins/Healthchecks.io/v1/defaultContent/check.dash.json b/plugins/Healthchecks.io/v1/defaultContent/check.dash.json new file mode 100644 index 00000000..652b1a46 --- /dev/null +++ b/plugins/Healthchecks.io/v1/defaultContent/check.dash.json @@ -0,0 +1,423 @@ +{ + "name": "Check", + "schemaVersion": "1.5", + "timeframe": "last7days", + "variables": ["{{variables.[Healthchecks Check]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "779bc7d7-5ebb-459c-9040-9b5afcdbd625", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Check Properties", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Healthchecks Checks]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Healthchecks Check]}}" + }, + "variables": ["{{variables.[Healthchecks Check]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "description", + "kind", + "schedule", + "tz", + "checkTags", + "slug", + "link" + ], + "hiddenColumns": [ + "id", + "links", + "type", + "label", + "sourceId", + "sourceType", + "sourceName", + "status", + "timeout", + "grace" + ], + "columnDisplayNames": { + "tz": "Time zone", + "checkTags": "Tags" + } + } + } + } + } + }, + { + "i": "9eb7a9df-6776-4b4c-9d1c-a496e254e2cc", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Status", + "description": "Current status of this check; select it to open the check in Healthchecks.io", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "checks": { + "variable": "{{variables.[Healthchecks Check]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Checks]}}" + } + } + }, + "variables": ["{{variables.[Healthchecks Check]}}"], + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "status", + "sublabel": "status[Expanded].rawState", + "linkColumn": "checkUrl", + "columns": 1 + } + } + } + } + }, + { + "i": "b69d52d5-eb88-49fd-9716-f7050a546e37", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Time Since Last Ping", + "description": "How long ago this check last received a ping", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "checks": { + "variable": "{{variables.[Healthchecks Check]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Checks]}}" + } + } + }, + "variables": ["{{variables.[Healthchecks Check]}}"], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "secondsSinceLastPing", + "comparisonColumn": "none", + "label": "Since last ping" + } + } + } + } + }, + { + "i": "e374f82e-32dd-4ed7-960f-80311c6e0f7c", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Ping Timing", + "description": "When the next ping is due and how long the check waits before alerting", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "checks": { + "variable": "{{variables.[Healthchecks Check]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Checks]}}" + } + } + }, + "variables": ["{{variables.[Healthchecks Check]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "next_ping", + "last_ping", + "timeout", + "grace", + "started" + ], + "hiddenColumns": [ + "name", + "status", + "status[Expanded].rawState", + "secondsSinceLastPing", + "kind", + "schedule", + "tz", + "tags", + "slug", + "desc", + "n_pings", + "checkUrl" + ] + } + } + } + } + }, + { + "i": "915c90a2-9c97-4d5b-9e6f-b200ad07434d", + "x": 1, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Availability", + "description": "Share of the timeframe this check spent up and down", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[statusHistory]}}", + "name": "statusHistory", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["status", "uniqueValues"]], + "aggregate": [ + { + "type": "sum", + "names": ["durationSeconds"] + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Healthchecks Checks]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Healthchecks Check]}}" + }, + "variables": ["{{variables.[Healthchecks Check]}}"], + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "durationSeconds_sum", + "labelColumn": "status_uniqueValues", + "hideCenterValue": true, + "showValuesAsPercentage": true, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "46be007d-ba58-447f-a33c-9ccbb5b07e20", + "x": 2, + "y": 2, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Status History", + "description": "Up and down periods of this check, newest first", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[statusHistory]}}", + "name": "statusHistory", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["start", "desc"]] + } + }, + "scope": { + "scope": "{{scopes.[Healthchecks Checks]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Healthchecks Check]}}" + }, + "variables": ["{{variables.[Healthchecks Check]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "start", + "end", + "durationSeconds", + "ongoing" + ], + "hiddenColumns": [ + "checkName", + "status[Expanded].rawState" + ] + } + } + } + } + }, + { + "i": "76113f71-3c2a-4caf-a842-7c5f290c1738", + "x": 0, + "y": 4, + "w": 4, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Ping Results", + "description": "Pings received per day, by type", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pings]}}", + "name": "pings", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["date_byDay", "asc"]] + }, + "group": { + "by": [["date", "byDay"], ["type", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + } + }, + "scope": { + "scope": "{{scopes.[Healthchecks Checks]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Healthchecks Check]}}" + }, + "variables": ["{{variables.[Healthchecks Check]}}"], + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "date_byDay", + "yAxisData": ["count"], + "xAxisGroup": "type_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "Pings", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": true, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "c46e4213-b49c-4499-90d4-365fc05265aa", + "x": 0, + "y": 6, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Pings", + "description": "Pings received by this check, newest first", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pings]}}", + "name": "pings", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["date", "desc"]] + } + }, + "scope": { + "scope": "{{scopes.[Healthchecks Checks]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Healthchecks Check]}}" + }, + "variables": ["{{variables.[Healthchecks Check]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "date", + "result", + "type", + "duration", + "exitstatus", + "method", + "scheme", + "remote_addr", + "ua" + ], + "hiddenColumns": ["checkName"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Healthchecks.io/v1/defaultContent/manifest.json b/plugins/Healthchecks.io/v1/defaultContent/manifest.json new file mode 100644 index 00000000..a54523d1 --- /dev/null +++ b/plugins/Healthchecks.io/v1/defaultContent/manifest.json @@ -0,0 +1,16 @@ +{ + "items": [ + { + "name": "overview", + "type": "dashboard" + }, + { + "name": "check", + "type": "dashboard" + }, + { + "name": "tag", + "type": "dashboard" + } + ] +} diff --git a/plugins/Healthchecks.io/v1/defaultContent/overview.dash.json b/plugins/Healthchecks.io/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..33f5dda0 --- /dev/null +++ b/plugins/Healthchecks.io/v1/defaultContent/overview.dash.json @@ -0,0 +1,358 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "none", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "83bf613c-b0f7-4193-a225-8dc8c6146918", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Down", + "description": "Checks that missed their expected ping and the grace time that follows it", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "error" + } + ] + }, + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Checks" + } + } + } + } + }, + { + "i": "d55882c2-05f6-421f-96a5-9b0e7052f767", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Late", + "description": "Checks past their expected ping time but still within their grace time", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "warning" + } + ] + }, + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Checks" + } + } + } + } + }, + { + "i": "443f0c9c-7f20-4bc2-a258-98673d1cdbc4", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Up", + "description": "Checks that pinged on time", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "success" + } + ] + }, + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Checks" + } + } + } + } + }, + { + "i": "286aa9f6-8363-48a6-8749-1509b1db2d52", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Paused", + "description": "Checks that are paused and not being monitored", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "unmonitored" + } + ] + }, + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Checks" + } + } + } + } + }, + { + "i": "207fdac3-f9ad-4b44-8b01-a531df5cdd76", + "x": 0, + "y": 2, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks by Status", + "description": "Current status of every check", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["status", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "status_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "adeb46cd-d28e-4d8d-ac21-0f3be515de75", + "x": 1, + "y": 2, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Needing Attention", + "description": "Checks that are down or late, longest without a ping first", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "error" + }, + { + "column": "status", + "operation": "equals", + "value": "warning" + } + ] + }, + "sort": { + "by": [["secondsSinceLastPing", "desc"]] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "status", + "secondsSinceLastPing", + "last_ping", + "next_ping", + "grace", + "tags", + "checkUrl" + ], + "hiddenColumns": [ + "status[Expanded].rawState", + "started", + "kind", + "timeout", + "schedule", + "tz", + "slug", + "desc", + "n_pings" + ] + } + } + } + } + }, + { + "i": "b16ae127-5eff-4302-bc9a-34c2c5232123", + "x": 0, + "y": 5, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "All Checks", + "description": "Every check in the project with its schedule and latest ping", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["name", "asc"]] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "status", + "last_ping", + "next_ping", + "kind", + "timeout", + "schedule", + "tz", + "grace", + "tags", + "checkUrl" + ], + "hiddenColumns": [ + "status[Expanded].rawState", + "started", + "secondsSinceLastPing", + "slug", + "desc", + "n_pings" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Healthchecks.io/v1/defaultContent/scopes.json b/plugins/Healthchecks.io/v1/defaultContent/scopes.json new file mode 100644 index 00000000..c426411a --- /dev/null +++ b/plugins/Healthchecks.io/v1/defaultContent/scopes.json @@ -0,0 +1,32 @@ +[ + { + "name": "Healthchecks Checks", + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Check"] + } + }, + "variable": { + "name": "Healthchecks Check", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Healthchecks Tags", + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Tag"] + } + }, + "variable": { + "name": "Healthchecks Tag", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Healthchecks.io/v1/defaultContent/tag.dash.json b/plugins/Healthchecks.io/v1/defaultContent/tag.dash.json new file mode 100644 index 00000000..b9feef2d --- /dev/null +++ b/plugins/Healthchecks.io/v1/defaultContent/tag.dash.json @@ -0,0 +1,415 @@ +{ + "name": "Tag", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": ["{{variables.[Healthchecks Tag]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "97d5fba2-a2a5-4a9e-89f7-af3197e77cde", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Down", + "description": "Checks with this tag that missed their expected ping and the grace time that follows it", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "tag": { + "variable": "{{variables.[Healthchecks Tag]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Tags]}}" + } + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "error" + } + ] + }, + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "variables": ["{{variables.[Healthchecks Tag]}}"], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Checks" + } + } + } + } + }, + { + "i": "17f483b1-6f66-4e0d-87bb-8816dc423cdc", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Late", + "description": "Checks with this tag past their expected ping time but still within their grace time", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "tag": { + "variable": "{{variables.[Healthchecks Tag]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Tags]}}" + } + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "warning" + } + ] + }, + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "variables": ["{{variables.[Healthchecks Tag]}}"], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Checks" + } + } + } + } + }, + { + "i": "38cf21b1-b5bd-4f1c-a320-d6dfd30cd7d0", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Up", + "description": "Checks with this tag that pinged on time", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "tag": { + "variable": "{{variables.[Healthchecks Tag]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Tags]}}" + } + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "success" + } + ] + }, + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "variables": ["{{variables.[Healthchecks Tag]}}"], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Checks" + } + } + } + } + }, + { + "i": "0b3011f5-b5bf-4dd8-9cbb-14a34672d1e3", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Paused", + "description": "Checks with this tag that are paused and not being monitored", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "tag": { + "variable": "{{variables.[Healthchecks Tag]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Tags]}}" + } + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "unmonitored" + } + ] + }, + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "variables": ["{{variables.[Healthchecks Tag]}}"], + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Checks" + } + } + } + } + }, + { + "i": "f4be1f50-a200-4696-8d91-5159449973f1", + "x": 0, + "y": 2, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks by Status", + "description": "Current status of every check with this tag", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "tag": { + "variable": "{{variables.[Healthchecks Tag]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Tags]}}" + } + }, + "group": { + "by": [["status", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + } + }, + "variables": ["{{variables.[Healthchecks Tag]}}"], + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "status_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "e802ea68-8877-46a4-bbf9-aa006de55049", + "x": 1, + "y": 2, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks Needing Attention", + "description": "Checks with this tag that are down or late, longest without a ping first", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "tag": { + "variable": "{{variables.[Healthchecks Tag]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Tags]}}" + } + }, + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "status", + "operation": "equals", + "value": "error" + }, + { + "column": "status", + "operation": "equals", + "value": "warning" + } + ] + }, + "sort": { + "by": [["secondsSinceLastPing", "desc"]] + } + }, + "variables": ["{{variables.[Healthchecks Tag]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "status", + "secondsSinceLastPing", + "last_ping", + "next_ping", + "grace", + "tags", + "checkUrl" + ], + "hiddenColumns": [ + "status[Expanded].rawState", + "started", + "kind", + "timeout", + "schedule", + "tz", + "slug", + "desc", + "n_pings" + ] + } + } + } + } + }, + { + "i": "5369d44e-4b07-41ad-8193-c7e633937d41", + "x": 0, + "y": 5, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Checks", + "description": "Every check with this tag, with its schedule and latest ping", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[checks]}}", + "name": "checks", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "tag": { + "variable": "{{variables.[Healthchecks Tag]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Healthchecks Tags]}}" + } + }, + "sort": { + "by": [["name", "asc"]] + } + }, + "variables": ["{{variables.[Healthchecks Tag]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "status", + "last_ping", + "next_ping", + "kind", + "timeout", + "schedule", + "tz", + "grace", + "tags", + "checkUrl" + ], + "hiddenColumns": [ + "status[Expanded].rawState", + "started", + "secondsSinceLastPing", + "slug", + "desc", + "n_pings" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Healthchecks.io/v1/docs/README.md b/plugins/Healthchecks.io/v1/docs/README.md new file mode 100644 index 00000000..7777400a --- /dev/null +++ b/plugins/Healthchecks.io/v1/docs/README.md @@ -0,0 +1,61 @@ +Monitor your [Healthchecks.io](https://healthchecks.io) cron jobs, scheduled tasks and heartbeats in SquaredUp: current check status, up/down history and job run times, using the [Healthchecks.io Management API](https://healthchecks.io/docs/api/). Self-hosted [Healthchecks](https://github.com/healthchecks/healthchecks) instances are supported too. + +## Setup + +You will need a Healthchecks.io **project API key**. API keys belong to a single project, so add one data source per project you want to monitor. + +1. Sign in to [Healthchecks.io](https://healthchecks.io/accounts/login/) and open the project to monitor. +2. Go to **Settings** (the project's settings page) and find the **API Access** section. +3. Click **Create API Keys** if the project has no keys yet. Healthchecks.io creates a **read-write** key and a **read-only** key. +4. Copy the **read-only** key and paste it into the **API key** field. The plugin never changes anything in Healthchecks.io, and a read-only key covers everything except the ping log (see [Known limitations](#known-limitations)). Use the read-write key instead if you want job run times and the ping log. +5. Leave **Server URL** as `https://healthchecks.io`, unless you run your own Healthchecks instance. + +> ⚠️ The **Ping key** shown on the same settings page is used to *send* pings and does not work with the Management API. Use one of the **API keys**. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ---------------- | -------- | +| **API key** | A project API key, sent in the `X-Api-Key` header. Read-only or read-write. | Healthchecks.io → your project → **Settings → API Access**. | Yes | +| **Server URL** | The address of the Healthchecks instance. Defaults to `https://healthchecks.io`. | Change only for a self-hosted instance, e.g. `https://healthchecks.example.com`. | Yes | +| **Ignore certificate errors** | Skips TLS certificate validation. | Enable only for a self-hosted instance with a self-signed or private CA certificate. | No | + +On save, the plugin validates the configuration by listing the project's checks. An invalid key, a ping key in place of an API key, or a wrong server URL fails setup with an authentication error. + +## What this plugin monitors + +- **Check status**: whether each check is up, late (in its grace period), down, paused or new, when it last pinged and when the next ping is due. +- **Availability**: how long each check spent up and down over a timeframe, and the outages in between. +- **Job runs**: every ping a check received, with success or failure, source IP and, for jobs that send a start signal, how long the job ran. + +The out-of-the-box dashboards include a project-wide **Overview**, a **Check** perspective for each check and a **Tag** perspective showing every check with a given tag. + +## Data streams + +- **Checks**: current status, schedule and last ping of every check in the project. **Filter by check** narrows it to selected checks; **Filter by tag** narrows it to checks that have every selected tag. +- **Status History**: up and down periods of a check over the selected timeframe, one row per period. Per check. +- **Pings**: pings received by a check over the selected timeframe, including job run time and exit status where reported. Per check, and needs a read-write API key. + +## What gets indexed + +| Object type | API source | Represents | +| ----------- | ---------- | ---------- | +| **Check** | `GET /api/v3/checks/` | A Healthchecks.io check: a cron job, scheduled task or heartbeat being monitored. | +| **Tag** | `GET /api/v3/badges/` | A tag used by one or more checks in the project. | + +Each check is indexed with its slug, kind (simple, cron or OnCalendar), period or schedule, time zone, grace time, description, tags and a link to the check in Healthchecks.io. Tags are indexed individually, so you can build scopes such as "all checks tagged `prod`". Objects are refreshed every 12 hours. The **Checks** stream always shows live status. + +**Relationships:** each Check **has tag** each Tag it carries (Tag **tag of** Check). Relationships are made within a data source, so a tag used in two projects appears as one Tag per project, each linked to its own project's checks. + +## Known limitations + +- **The ping log needs a read-write key.** Healthchecks.io read-only keys can't read pings, so **Pings** tiles show an error with a read-only key. Everything else works with either key type. +- **No links to checks with a read-only key.** Healthchecks.io leaves the check UUID out of read-only API responses, so the **Link** column and the link to each check in Healthchecks.io are empty. +- **The ping log is capped.** Healthchecks.io returns only the newest 100 pings (free accounts) or 1,000 pings (paid accounts) per check. For frequently pinged checks, longer timeframes show only the most recent pings. +- **Job run time needs start signals.** Duration is only recorded when a job sends a `/start` ping before its success or failure ping. +- **Status history covers about three months.** Healthchecks.io keeps status changes for the current month and the two months before it, so **Status History** offers timeframes up to **This quarter**. Time before a check's earliest retained status change isn't shown. +- **Paused periods appear as down.** Healthchecks.io records status changes only as up or not up, so a period when a check was paused shows as down in **Status History**. +- **Rate limit.** Healthchecks.io allows 100 API requests per minute per project. **Status History** and **Pings** make one request per check, so keep those tiles scoped to a handful of checks rather than a whole large project. +- **One project per data source.** API keys belong to a single project. Add a data source for each project. +- **Changing key type re-creates checks.** A read-write key identifies checks by UUID and a read-only key by a different stable key. Switching between the two after setup re-imports every check as a new object, so dashboards scoped to specific checks need updating. +- **Read-only.** The plugin never creates, changes, pauses or deletes anything in Healthchecks.io. diff --git a/plugins/Healthchecks.io/v1/icon.svg b/plugins/Healthchecks.io/v1/icon.svg new file mode 100644 index 00000000..1632d509 --- /dev/null +++ b/plugins/Healthchecks.io/v1/icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plugins/Healthchecks.io/v1/indexDefinitions/default.json b/plugins/Healthchecks.io/v1/indexDefinitions/default.json new file mode 100644 index 00000000..352f8610 --- /dev/null +++ b/plugins/Healthchecks.io/v1/indexDefinitions/default.json @@ -0,0 +1,50 @@ +{ + "steps": [ + { + "name": "checks", + "dataStream": { + "name": "checks" + }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { + "value": "Check" + }, + "properties": [ + "slug", + "status", + "kind", + "timeout", + "grace", + "schedule", + "tz", + { + "description": "desc" + }, + { + "checkTags": "tags" + }, + { + "link": "checkUrl" + } + ] + } + }, + { + "name": "tags", + "dataStream": { + "name": "tags" + }, + "timeframe": "none", + "objectMapping": { + "id": "name", + "name": "name", + "type": { + "value": "Tag" + } + } + } + ] +} diff --git a/plugins/Healthchecks.io/v1/metadata.json b/plugins/Healthchecks.io/v1/metadata.json new file mode 100644 index 00000000..c22b35f7 --- /dev/null +++ b/plugins/Healthchecks.io/v1/metadata.json @@ -0,0 +1,59 @@ +{ + "name": "healthchecks-io", + "displayName": "Healthchecks.io", + "version": "1.0.0", + "author": { + "name": "@adamkinniburgh", + "type": "community" + }, + "description": "Monitor your cron jobs and scheduled tasks with check status, availability and job run times.", + "category": "Monitoring", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "healthchecks", + "healthchecks.io", + "cron", + "cron job", + "heartbeat", + "scheduled task", + "dead man's switch", + "uptime", + "monitoring" + ], + "objectTypes": ["Check", "Tag"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Healthchecks.io/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Healthchecks.io/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "{{ (serverUrl || 'https://healthchecks.io').trim().replace(/[/]+$/, '').replace(/[/]api([/]v[0-9]+)?$/i, '') }}/api/v3/", + "authMode": "none", + "headers": [ + { + "key": "X-Api-Key", + "value": "{{ apiKey.trim() }}" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "queryArgs": [], + "ignoreCertificateErrors": "{{ typeof ignoreCertificateErrors !== 'undefined' && ignoreCertificateErrors === true }}" + } + } +} diff --git a/plugins/Healthchecks.io/v1/ui.json b/plugins/Healthchecks.io/v1/ui.json new file mode 100644 index 00000000..be7f0b6c --- /dev/null +++ b/plugins/Healthchecks.io/v1/ui.json @@ -0,0 +1,34 @@ +[ + { + "type": "password", + "name": "apiKey", + "label": "API key", + "placeholder": "Enter the Healthchecks.io project API key", + "help": "Create a key under **Project Settings → API Access**. A read-only key covers everything except the ping log.", + "validation": { + "required": true + } + }, + { + "type": "text", + "name": "serverUrl", + "label": "Server URL", + "defaultValue": "https://healthchecks.io", + "placeholder": "https://healthchecks.io", + "help": "Change only when connecting to a self-hosted Healthchecks instance.", + "validation": { + "required": true, + "pattern": { + "value": "^https?://[^ ]+$", + "message": "Enter the full address of the Healthchecks instance, e.g. https://healthchecks.example.com" + } + } + }, + { + "type": "checkbox", + "name": "ignoreCertificateErrors", + "label": "Ignore certificate errors", + "defaultValue": false, + "help": "Enable when connecting to a self-hosted instance that uses a self-signed or private CA certificate." + } +] From db2dbc9691317c70e14e472a25569715037f1dda Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Mon, 12 Oct 2026 11:31:12 +0100 Subject: [PATCH 2/4] Route Healthchecks.io reviews to @adamkinniburgh Co-Authored-By: Claude Opus 5.5 --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4c0b7722..f659dad1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -34,5 +34,6 @@ plugins/WorldCup2026/* @TimWheeler-SQUP plugins/Notion/* @clarkd plugins/Algolia/* @andrewmumblebee plugins/Vercel/* @andrewmumblebee +plugins/Healthchecks.io/* @adamkinniburgh From a9e07f6f3e0bbd821120cee8554deb032eb50cce Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Mon, 12 Oct 2026 11:55:40 +0100 Subject: [PATCH 3/4] Keep check identity stable across API key types Read-only keys identify a check by `unique_key` and read-write keys by `uuid`, so switching a data source from one to the other re-created every check and broke dashboards scoped to specific checks. That switch is a natural path, because the README recommends starting read-only and the ping log needs read-write. Healthchecks.io derives `unique_key` as the SHA-1 of the first 16 hex digits of the UUID, so the checks script now computes it for read-write keys with `crypto.subtle` and uses it as the id for both key types. I checked the derived values against what a read-only key returns for the same project. The flips endpoint accepts `unique_key` with either key, but the ping log route only accepts UUIDs, so the UUID is indexed as its own property for `pings`, and the pings error handler now detects a read-only key by that property being absent. Read-only checks also get a link now, through Healthchecks.io's `/cloaked//` route (which sends a signed-in user to the check), rooted on `badge_url` so self-hosted instances under a subpath keep it. The unused ping `body_url` column is dropped. Co-Authored-By: Claude Opus 5.5 --- .../v1/dataStreams/checks.json | 13 +++++- .../Healthchecks.io/v1/dataStreams/pings.json | 7 +-- .../v1/dataStreams/scripts/checks.js | 43 +++++++++++++------ .../scripts/errorHandling/pings.js | 9 ++-- plugins/Healthchecks.io/v1/docs/README.md | 4 +- .../v1/indexDefinitions/default.json | 1 + 6 files changed, 49 insertions(+), 28 deletions(-) diff --git a/plugins/Healthchecks.io/v1/dataStreams/checks.json b/plugins/Healthchecks.io/v1/dataStreams/checks.json index cd5132fb..d708219e 100644 --- a/plugins/Healthchecks.io/v1/dataStreams/checks.json +++ b/plugins/Healthchecks.io/v1/dataStreams/checks.json @@ -160,11 +160,22 @@ "displayName": "API URL", "visible": false }, + { + "name": "uuid", + "displayName": "UUID", + "shape": "string", + "visible": false + }, + { + "name": "badge_url", + "displayName": "Badge URL", + "visible": false + }, { "name": "checkUrl", "displayName": "Link", "computed": true, - "valueExpression": "{{ $['update_url'] ? $['update_url'].replace(/[/]api[/]v[0-9]+[/]checks[/]/, '/checks/') + '/details/' : null }}", + "valueExpression": "{{ $['update_url'] ? $['update_url'].replace(/[/]api[/]v[0-9]+[/]checks[/]/, '/checks/') + '/details/' : ($['badge_url'] ? $['badge_url'].replace(/[/]b[/].*$/, '') + '/cloaked/' + $['id'] + '/' : null) }}", "shape": ["url", { "label": "Open in Healthchecks.io" }] }, { diff --git a/plugins/Healthchecks.io/v1/dataStreams/pings.json b/plugins/Healthchecks.io/v1/dataStreams/pings.json index 51706eb5..1d5edab0 100644 --- a/plugins/Healthchecks.io/v1/dataStreams/pings.json +++ b/plugins/Healthchecks.io/v1/dataStreams/pings.json @@ -6,7 +6,7 @@ "baseDataSourceName": "httpRequestScopedSingle", "config": { "httpMethod": "get", - "endpointPath": "checks/{{object.rawId}}/pings/", + "endpointPath": "checks/{{object.uuid || object.rawId}}/pings/", "paging": { "mode": "none" }, @@ -112,11 +112,6 @@ "shape": "number", "visible": false }, - { - "name": "body_url", - "displayName": "Body URL", - "visible": false - }, { "sourceId": "checkId", "sourceType": "Check", diff --git a/plugins/Healthchecks.io/v1/dataStreams/scripts/checks.js b/plugins/Healthchecks.io/v1/dataStreams/scripts/checks.js index 7d25de89..dc7f8c54 100644 --- a/plugins/Healthchecks.io/v1/dataStreams/scripts/checks.js +++ b/plugins/Healthchecks.io/v1/dataStreams/scripts/checks.js @@ -1,6 +1,18 @@ -// Read-write keys return `uuid`; read-only keys omit it and return `unique_key` -// instead. Both are accepted by the per-check endpoints, so either is a stable id. -const idOf = (c) => c.uuid || c.unique_key; +// Read-only keys return a check's `unique_key`; read-write keys return its +// `uuid` instead. Healthchecks.io derives `unique_key` from the UUID (SHA-1 of +// its first 16 hex digits), so computing it here gives every check the same +// id whichever key type the data source uses. +const sha1Hex = async (text) => { + const digest = await crypto.subtle.digest( + "SHA-1", + new TextEncoder().encode(text), + ); + return Array.from(new Uint8Array(digest)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +}; +const uniqueKeyOf = (c) => + c.unique_key || sha1Hex(c.uuid.replace(/-/g, "").slice(0, 16)); // Optional `checks` object picker (or a bound dashboard variable). Empty or // absent means every check in the project. @@ -19,13 +31,18 @@ const kindOf = (schedule) => { return schedule.trim().split(/\s+/).length === 5 ? "Cron" : "OnCalendar"; }; -result = ((data && data.checks) || []) - .filter((c) => !selectedIds.size || selectedIds.has(idOf(c))) - .map((c) => ({ - ...c, - id: idOf(c), - name: c.name || c.slug || idOf(c), - kind: kindOf(c.schedule), - // The API returns tags as one space-separated string. - tags: (c.tags || "").split(/\s+/).filter(Boolean), - })); +const checks = await Promise.all( + ((data && data.checks) || []).map(async (c) => { + const id = await uniqueKeyOf(c); + return { + ...c, + id, + name: c.name || c.slug || id, + kind: kindOf(c.schedule), + // The API returns tags as one space-separated string. + tags: (c.tags || "").split(/\s+/).filter(Boolean), + }; + }), +); + +result = checks.filter((c) => !selectedIds.size || selectedIds.has(c.id)); diff --git a/plugins/Healthchecks.io/v1/dataStreams/scripts/errorHandling/pings.js b/plugins/Healthchecks.io/v1/dataStreams/scripts/errorHandling/pings.js index af9263a2..b315755b 100644 --- a/plugins/Healthchecks.io/v1/dataStreams/scripts/errorHandling/pings.js +++ b/plugins/Healthchecks.io/v1/dataStreams/scripts/errorHandling/pings.js @@ -4,13 +4,12 @@ const apiMsg = data && typeof data === "object" && data.error; const readOnlyMsg = "The ping log needs a Healthchecks.io read-write API key (read-only keys cannot read pings)"; -// Read-only keys identify checks by a 40-character `unique_key`. The ping log -// route only accepts UUIDs, so those requests 404 rather than returning 401. +// Only read-write keys expose a check's UUID, and the ping log route only +// accepts UUIDs, so read-only requests 404 rather than returning 401. const object = context && context.objects && context.objects[0]; -const rawId = object && (Array.isArray(object.rawId) ? object.rawId[0] : object.rawId); -const isReadOnlyId = typeof rawId === "string" && /^[0-9a-f]{40}$/i.test(rawId); +const uuid = object && (Array.isArray(object.uuid) ? object.uuid[0] : object.uuid); -if (isReadOnlyId || response.status === 401 || response.status === 403) { +if (!uuid || response.status === 401 || response.status === 403) { result = readOnlyMsg + (apiMsg ? ": " + apiMsg : ""); } else if (response.status === 404) { result = "Check not found in Healthchecks.io (it may have been deleted)"; diff --git a/plugins/Healthchecks.io/v1/docs/README.md b/plugins/Healthchecks.io/v1/docs/README.md index 7777400a..87d8f5b8 100644 --- a/plugins/Healthchecks.io/v1/docs/README.md +++ b/plugins/Healthchecks.io/v1/docs/README.md @@ -7,7 +7,7 @@ You will need a Healthchecks.io **project API key**. API keys belong to a single 1. Sign in to [Healthchecks.io](https://healthchecks.io/accounts/login/) and open the project to monitor. 2. Go to **Settings** (the project's settings page) and find the **API Access** section. 3. Click **Create API Keys** if the project has no keys yet. Healthchecks.io creates a **read-write** key and a **read-only** key. -4. Copy the **read-only** key and paste it into the **API key** field. The plugin never changes anything in Healthchecks.io, and a read-only key covers everything except the ping log (see [Known limitations](#known-limitations)). Use the read-write key instead if you want job run times and the ping log. +4. Copy the **read-only** key and paste it into the **API key** field. The plugin never changes anything in Healthchecks.io, and a read-only key covers everything except the ping log (see [Known limitations](#known-limitations)). Use the read-write key instead if you want job run times and the ping log. You can switch key type later without losing checks or dashboards. 5. Leave **Server URL** as `https://healthchecks.io`, unless you run your own Healthchecks instance. > ⚠️ The **Ping key** shown on the same settings page is used to *send* pings and does not work with the Management API. Use one of the **API keys**. @@ -50,12 +50,10 @@ Each check is indexed with its slug, kind (simple, cron or OnCalendar), period o ## Known limitations - **The ping log needs a read-write key.** Healthchecks.io read-only keys can't read pings, so **Pings** tiles show an error with a read-only key. Everything else works with either key type. -- **No links to checks with a read-only key.** Healthchecks.io leaves the check UUID out of read-only API responses, so the **Link** column and the link to each check in Healthchecks.io are empty. - **The ping log is capped.** Healthchecks.io returns only the newest 100 pings (free accounts) or 1,000 pings (paid accounts) per check. For frequently pinged checks, longer timeframes show only the most recent pings. - **Job run time needs start signals.** Duration is only recorded when a job sends a `/start` ping before its success or failure ping. - **Status history covers about three months.** Healthchecks.io keeps status changes for the current month and the two months before it, so **Status History** offers timeframes up to **This quarter**. Time before a check's earliest retained status change isn't shown. - **Paused periods appear as down.** Healthchecks.io records status changes only as up or not up, so a period when a check was paused shows as down in **Status History**. - **Rate limit.** Healthchecks.io allows 100 API requests per minute per project. **Status History** and **Pings** make one request per check, so keep those tiles scoped to a handful of checks rather than a whole large project. - **One project per data source.** API keys belong to a single project. Add a data source for each project. -- **Changing key type re-creates checks.** A read-write key identifies checks by UUID and a read-only key by a different stable key. Switching between the two after setup re-imports every check as a new object, so dashboards scoped to specific checks need updating. - **Read-only.** The plugin never creates, changes, pauses or deletes anything in Healthchecks.io. diff --git a/plugins/Healthchecks.io/v1/indexDefinitions/default.json b/plugins/Healthchecks.io/v1/indexDefinitions/default.json index 352f8610..435f73f7 100644 --- a/plugins/Healthchecks.io/v1/indexDefinitions/default.json +++ b/plugins/Healthchecks.io/v1/indexDefinitions/default.json @@ -13,6 +13,7 @@ "value": "Check" }, "properties": [ + "uuid", "slug", "status", "kind", From b934883e8d8afb09d5a555b8d61f2bd9dccf357b Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Mon, 12 Oct 2026 11:55:41 +0100 Subject: [PATCH 4/4] Cover nested Healthchecks.io files in CODEOWNERS A `dir/*` pattern only matches files directly inside the directory, so the entry didn't apply to anything under `v1/`. Co-Authored-By: Claude Opus 5.5 --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f659dad1..1558ccea 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -34,6 +34,6 @@ plugins/WorldCup2026/* @TimWheeler-SQUP plugins/Notion/* @clarkd plugins/Algolia/* @andrewmumblebee plugins/Vercel/* @andrewmumblebee -plugins/Healthchecks.io/* @adamkinniburgh +plugins/Healthchecks.io/ @adamkinniburgh