From f0400b0d1be21810ceee904afe0c3f3ec87d6e08 Mon Sep 17 00:00:00 2001 From: Steven Martins <139889226+steven-s-martins@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:49:17 -0600 Subject: [PATCH 1/2] feat: enhance Healthchecks with up/down livestats Show check up/down counts on the tile via the Management API, matching other uptime-style enhanced apps. --- Healthchecks/Healthchecks.php | 65 +++++++++++++++++++++++++++++++- Healthchecks/app.json | 2 +- Healthchecks/config.blade.php | 16 ++++++++ Healthchecks/livestats.blade.php | 10 +++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 Healthchecks/config.blade.php create mode 100644 Healthchecks/livestats.blade.php diff --git a/Healthchecks/Healthchecks.php b/Healthchecks/Healthchecks.php index 05b4db0a7d..2997da432f 100644 --- a/Healthchecks/Healthchecks.php +++ b/Healthchecks/Healthchecks.php @@ -2,6 +2,69 @@ namespace App\SupportedApps\Healthchecks; -class Healthchecks extends \App\SupportedApps +class Healthchecks extends \App\SupportedApps implements \App\EnhancedApps { + public $config; + + public function __construct() + { + } + + public function test() + { + $test = parent::appTest($this->url('api/v3/checks/'), $this->getAttrs()); + echo $test->status; + } + + public function livestats() + { + $data = [ + 'up' => 0, + 'down' => 0, + ]; + + // Single authenticated GET. execute() returns null on a failed + // connection (it never throws), so guard before reading the body. + $res = parent::execute($this->url('api/v3/checks/'), $this->getAttrs()); + if ($res === null) { + return parent::getLiveStats('inactive', $data); + } + + // Management API returns { "checks": [ ... ] }; each check has + // status of new, up, grace, down, or paused. + $body = json_decode($res->getBody()); + $checks = is_object($body) ? $body->checks : []; + if (!is_array($checks)) { + return parent::getLiveStats('inactive', $data); + } + + foreach ($checks as $check) { + if (!isset($check->status)) { + continue; + } + if ($check->status === 'up') { + $data['up']++; + } elseif ($check->status === 'down') { + $data['down']++; + } + } + + return parent::getLiveStats('inactive', $data); + } + + private function getAttrs() + { + return [ + 'headers' => [ + 'Accept' => 'application/json', + 'X-Api-Key' => ($this->config->apikey ?? ''), + ], + ]; + } + + public function url($endpoint) + { + $api_url = parent::normaliseurl($this->config->url) . $endpoint; + return $api_url; + } } diff --git a/Healthchecks/app.json b/Healthchecks/app.json index e6f4542293..9a80bb28da 100644 --- a/Healthchecks/app.json +++ b/Healthchecks/app.json @@ -4,7 +4,7 @@ "website": "https://healthchecks.io", "license": "BSD 3-Clause \"New\" or \"Revised\" License", "description": "Instant alerts when your cron jobs fail silently.", - "enhanced": false, + "enhanced": true, "tile_background": "light", "icon": "healthchecks.png" } diff --git a/Healthchecks/config.blade.php b/Healthchecks/config.blade.php new file mode 100644 index 0000000000..ee79f8ac85 --- /dev/null +++ b/Healthchecks/config.blade.php @@ -0,0 +1,16 @@ +

{{ __('app.apps.config') }} ({{ __('app.optional') }}) @include('items.enable')

+
+
+ + {!! Form::text('config[override_url]', isset($item) ? $item->getconfig()->override_url : null, ['placeholder' => __('app.apps.override'), 'id' => 'override_url', 'class' => 'form-control']) !!} +
+
+ + {!! Form::text('config[apikey]', isset($item) ? ($item->getconfig()->apikey ?? null) : null, ['placeholder' => __('app.apps.apikey'), 'data-config' => 'apikey', 'class' => 'form-control config-item']) !!} +
+
+ +
+ {{-- Up/down counts change moderately; refresh on the 30s "data only" cadence, not every 5s. --}} + {!! Form::hidden('config[dataonly]', '1') !!} +
diff --git a/Healthchecks/livestats.blade.php b/Healthchecks/livestats.blade.php new file mode 100644 index 0000000000..109206a02a --- /dev/null +++ b/Healthchecks/livestats.blade.php @@ -0,0 +1,10 @@ + From bb35076745f8c365234567818b7c3d550be0e896 Mon Sep 17 00:00:00 2001 From: KodeStar Date: Fri, 11 Sep 2026 14:53:35 +0100 Subject: [PATCH 2/2] Guard Healthchecks livestats against error responses The Management API returns 401 with a { "error": ... } body when the API key is missing or revoked, and execute() is configured with http_errors => false, so that body reaches livestats() as a stdClass with no "checks" property. Reading it directly raised an undefined property warning, which Laravel promotes to an ErrorException, so get_stats returned a 500 and the tile broke instead of falling back to zeroed counts. Coalesce the property to null so a malformed or error response leaves the tile inactive, and flip the status to active only when a real checks array came back - matching the KuvaszUptime pattern this app follows. --- Healthchecks/Healthchecks.php | 43 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Healthchecks/Healthchecks.php b/Healthchecks/Healthchecks.php index 2997da432f..fed0a889d9 100644 --- a/Healthchecks/Healthchecks.php +++ b/Healthchecks/Healthchecks.php @@ -18,6 +18,7 @@ public function test() public function livestats() { + $status = 'inactive'; $data = [ 'up' => 0, 'down' => 0, @@ -26,30 +27,30 @@ public function livestats() // Single authenticated GET. execute() returns null on a failed // connection (it never throws), so guard before reading the body. $res = parent::execute($this->url('api/v3/checks/'), $this->getAttrs()); - if ($res === null) { - return parent::getLiveStats('inactive', $data); - } - - // Management API returns { "checks": [ ... ] }; each check has - // status of new, up, grace, down, or paused. - $body = json_decode($res->getBody()); - $checks = is_object($body) ? $body->checks : []; - if (!is_array($checks)) { - return parent::getLiveStats('inactive', $data); - } - - foreach ($checks as $check) { - if (!isset($check->status)) { - continue; - } - if ($check->status === 'up') { - $data['up']++; - } elseif ($check->status === 'down') { - $data['down']++; + if ($res !== null) { + // Management API returns { "checks": [ ... ] }; each check has a + // status of new, up, grace, down, or paused. Errors (a bad API + // key returns 401 with { "error": ... }) carry no "checks" key, + // so coalesce to null rather than reading the property directly + // and leave the tile inactive in that case. + $body = json_decode($res->getBody()); + $checks = $body->checks ?? null; + if (is_array($checks)) { + $status = 'active'; + foreach ($checks as $check) { + if (!isset($check->status)) { + continue; + } + if ($check->status === 'up') { + $data['up']++; + } elseif ($check->status === 'down') { + $data['down']++; + } + } } } - return parent::getLiveStats('inactive', $data); + return parent::getLiveStats($status, $data); } private function getAttrs()