From acd54efaf1f66ee2ff647325f41f86abfab4dce4 Mon Sep 17 00:00:00 2001 From: GOUKI9999 Date: Wed, 7 Aug 2024 01:14:29 +0800 Subject: [PATCH 1/8] Fix Monit App replace queue and speed by number of running /failed service --- Monit/Monit.php | 99 +++++++++++++++++++++++++++++++-------- Monit/livestats.blade.php | 8 ++-- 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/Monit/Monit.php b/Monit/Monit.php index e146bb6bd0..4125e3e9b2 100644 --- a/Monit/Monit.php +++ b/Monit/Monit.php @@ -2,36 +2,95 @@ namespace App\SupportedApps\Monit; -class Monit extends \App\SupportedApps implements \App\EnhancedApps +class Monit extends \App\SupportedApps { - public $config; - - //protected $login_first = true; // Uncomment if api requests need to be authed first - //protected $method = 'POST'; // Uncomment if requests to the API should be set by POST - - public function __construct() - { - //$this->jar = new \GuzzleHttp\Cookie\CookieJar; // Uncomment if cookies need to be set - } - public function test() { - $test = parent::appTest($this->url("status")); - echo $test->status; + $response = $this->executeCurl($this->url('/_status?format=xml')); + if ($response['httpcode'] == 200) { + echo 'Successfully communicated with the API'; + } else { + echo 'Failed to connect to Monit. HTTP Status: ' . $response['httpcode']; + } } public function livestats() { - $status = "inactive"; - $res = parent::execute($this->url("status")); - $details = json_decode($res->getBody()); - + $status = 'inactive'; $data = []; + + $response = $this->executeCurl($this->url('/_status?format=xml')); + + if ($response['httpcode'] == 200) { + $xml = simplexml_load_string($response['response']); + $json = json_encode($xml); + $data = json_decode($json, true); + + // 计算运行的服务数量和失败的服务数量 + $running_services = 0; + $failed_services = 0; + + if (isset($data['service'])) { + if (isset($data['service'][0])) { + // 如果是多个服务的情况 + foreach ($data['service'] as $service) { + if (isset($service['status']) && $service['status'] == 0) { + $running_services++; + } else { + $failed_services++; + } + } + } else { + // 如果是单个服务的情况 + if (isset($data['service']['status']) && $data['service']['status'] == 0) { + $running_services++; + } else { + $failed_services++; + } + } + } + + $status = 'active'; + $data = [ + 'running_services' => $running_services, + 'failed_services' => $failed_services + ]; + } else { + $data = [ + 'error' => 'Failed to connect to Monit. HTTP Status: ' . $response['httpcode'] + ]; + } + + // 返回JSON格式数据 return parent::getLiveStats($status, $data); } - public function url($endpoint) + + private function url($endpoint) + { + $config = $this->config; + + $url = rtrim($config->url, '/'); + + return $url . $endpoint; + } + + private function executeCurl($url) { - $api_url = parent::normaliseurl($this->config->url) . $endpoint; - return $api_url; + $username = $this->config->username; + $password = $this->config->password; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_USERPWD, "$username:$password"); + $response = curl_exec($ch); + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return [ + 'response' => $response, + 'httpcode' => $httpcode + ]; } } diff --git a/Monit/livestats.blade.php b/Monit/livestats.blade.php index 0ed6877c81..20aec2db76 100644 --- a/Monit/livestats.blade.php +++ b/Monit/livestats.blade.php @@ -1,10 +1,10 @@ From ccd6a47133dad43b8382240f6009977cbb299f17 Mon Sep 17 00:00:00 2001 From: GOUKI9999 Date: Fri, 9 Aug 2024 16:01:50 +0800 Subject: [PATCH 2/8] Fix-for-Monit --- Monit/Monit.php | 51 ++++++++++++++++++++++++++++++++------- Monit/config.blade.php | 7 +++++- Monit/livestats.blade.php | 14 +++++------ 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/Monit/Monit.php b/Monit/Monit.php index 4125e3e9b2..51e9525e10 100644 --- a/Monit/Monit.php +++ b/Monit/Monit.php @@ -4,6 +4,18 @@ class Monit extends \App\SupportedApps { + public static function getAvailableStats() + { + return [ + 'running_services' => 'Running', + 'failed_services' => 'Failed', + 'load' => 'Load', + 'cpu' => 'CPU', + 'memory' => 'Memory', + 'swap' => 'Swap' + ]; + } + public function test() { $response = $this->executeCurl($this->url('/_status?format=xml')); @@ -18,7 +30,6 @@ public function livestats() { $status = 'inactive'; $data = []; - $response = $this->executeCurl($this->url('/_status?format=xml')); if ($response['httpcode'] == 200) { @@ -26,13 +37,15 @@ public function livestats() $json = json_encode($xml); $data = json_decode($json, true); - // 计算运行的服务数量和失败的服务数量 $running_services = 0; $failed_services = 0; + $load = 'N/A'; + $cpu = 'N/A'; + $memory = 'N/A'; + $swap = 'N/A'; if (isset($data['service'])) { if (isset($data['service'][0])) { - // 如果是多个服务的情况 foreach ($data['service'] as $service) { if (isset($service['status']) && $service['status'] == 0) { $running_services++; @@ -41,7 +54,6 @@ public function livestats() } } } else { - // 如果是单个服务的情况 if (isset($data['service']['status']) && $data['service']['status'] == 0) { $running_services++; } else { @@ -50,10 +62,24 @@ public function livestats() } } + foreach ($data['service'] as $service) { + if (isset($service['system'])) { + $load = $service['system']['load']['avg05'] ?? 'N/A'; + $cpu = isset($service['system']['cpu']['user']) ? $service['system']['cpu']['user'] . '%' : 'N/A'; + $memory = isset($service['system']['memory']['percent']) ? $service['system']['memory']['percent'] . '%' : 'N/A'; + $swap = isset($service['system']['swap']['percent']) ? $service['system']['swap']['percent'] . '%' : 'N/A'; + break; + } + } + $status = 'active'; $data = [ 'running_services' => $running_services, - 'failed_services' => $failed_services + 'failed_services' => $failed_services, + 'load' => $load, + 'cpu' => $cpu, + 'memory' => $memory, + 'swap' => $swap ]; } else { $data = [ @@ -61,16 +87,23 @@ public function livestats() ]; } - // 返回JSON格式数据 - return parent::getLiveStats($status, $data); + $visiblestats = []; + if (isset($this->config->availablestats)) { + foreach ($this->config->availablestats as $stat) { + $visiblestats[] = [ + 'title' => self::getAvailableStats()[$stat], + 'value' => $data[$stat] ?? 'N/A' + ]; + } + } + + return parent::getLiveStats($status, ['visiblestats' => $visiblestats]); } private function url($endpoint) { $config = $this->config; - $url = rtrim($config->url, '/'); - return $url . $endpoint; } diff --git a/Monit/config.blade.php b/Monit/config.blade.php index 26b4af285c..45c49c8bc9 100644 --- a/Monit/config.blade.php +++ b/Monit/config.blade.php @@ -1,4 +1,5 @@

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

+
@@ -12,7 +13,11 @@ {!! Form::input('password', 'config[password]', '', ['placeholder' => __('app.apps.password'), 'data-config' => 'password', 'class' => 'form-control config-item']) !!}
+
+ + {!! Form::select('config[availablestats][]', App\SupportedApps\Monit\Monit::getAvailableStats(), isset($item) && isset($item->getconfig()->availablestats) ? $item->getconfig()->availablestats : null, ['multiple' => 'multiple', 'class' => 'form-control config-item']) !!} +
-
+ \ No newline at end of file diff --git a/Monit/livestats.blade.php b/Monit/livestats.blade.php index 20aec2db76..b8eb263375 100644 --- a/Monit/livestats.blade.php +++ b/Monit/livestats.blade.php @@ -1,10 +1,8 @@ From 5b376109c16ec9689ac51367ce7c68301ae51c5c Mon Sep 17 00:00:00 2001 From: GOUKI9999 Date: Fri, 9 Aug 2024 16:04:04 +0800 Subject: [PATCH 3/8] Enhance-For-Netdata --- Netdata/Netdata.php | 104 +++++++++++++++++++++++++++++------- Netdata/config.blade.php | 5 ++ Netdata/livestats.blade.php | 14 +++-- 3 files changed, 96 insertions(+), 27 deletions(-) diff --git a/Netdata/Netdata.php b/Netdata/Netdata.php index bdfb937042..6cab82971d 100644 --- a/Netdata/Netdata.php +++ b/Netdata/Netdata.php @@ -4,38 +4,104 @@ class Netdata extends \App\SupportedApps implements \App\EnhancedApps { - private const ENDPOINT = "api/v1/info"; - - public $config; - - public function __construct() + public static function getAvailableStats() { + return [ + 'cpu' => 'CPU', + 'memory_free' => 'Mem Free', + 'memory_used' => 'Mem Used', + 'load1' => 'Load 1', + 'load5' => 'Load 5', + 'load15' => 'Load 15', + 'disk_in' => 'Disk In', + 'disk_out' => 'Disk Out', + 'network_in' => 'Net In', + 'network_out' => 'Net Out' + ]; } public function test() { - $test = parent::appTest($this->url(self::ENDPOINT)); - echo $test->status; + $response = $this->executeCurl($this->url('/api/v1/allmetrics?format=json')); + if ($response['httpcode'] == 200) { + echo 'Successfully communicated with the API'; + } else { + echo 'Failed to connect to Netdata. HTTP Status: ' . $response['httpcode']; + } } public function livestats() { - $status = "inactive"; - $res = parent::execute($this->url(self::ENDPOINT)); - $res = parent::execute($this->url(self::ENDPOINT)); - $details = json_decode($res->getBody()); - - $data = [ - "count_warning" => $details->alarms->warning, - "count_critical" => $details->alarms->critical, - ]; + $status = 'inactive'; + $data = []; + $response = $this->executeCurl($this->url('/api/v1/allmetrics?format=json')); + + if ($response['httpcode'] == 200) { + $json = json_decode($response['response'], true); + + // Extract values and format them + $cpu = isset($json['netdata.plugin_proc_cpu']['dimensions']['user']['value']) ? number_format($json['netdata.plugin_proc_cpu']['dimensions']['user']['value'], 2) . '%' : 'N/A'; + $memoryFree = isset($json['system.ram']['dimensions']['free']['value']) ? number_format($json['system.ram']['dimensions']['free']['value'], 2) . 'MiB' : 'N/A'; + $memoryUsed = isset($json['system.ram']['dimensions']['used']['value']) ? number_format($json['system.ram']['dimensions']['used']['value'], 2) . 'MiB' : 'N/A'; + $load1 = isset($json['system.load']['dimensions']['load1']['value']) ? $json['system.load']['dimensions']['load1']['value'] : 'N/A'; + $load5 = isset($json['system.load']['dimensions']['load5']['value']) ? $json['system.load']['dimensions']['load5']['value'] : 'N/A'; + $load15 = isset($json['system.load']['dimensions']['load15']['value']) ? $json['system.load']['dimensions']['load15']['value'] : 'N/A'; + $diskIn = isset($json['system.io']['dimensions']['in']['value']) ? number_format($json['system.io']['dimensions']['in']['value'], 2) . 'KB/s' : 'N/A'; + $diskOut = isset($json['system.io']['dimensions']['out']['value']) ? number_format($json['system.io']['dimensions']['out']['value'], 2) . 'KB/s' : 'N/A'; + $networkIn = isset($json['net.br-lan']['dimensions']['received']['value']) ? number_format($json['net.br-lan']['dimensions']['received']['value'], 2) . 'KB/s' : 'N/A'; + $networkOut = isset($json['net.br-lan']['dimensions']['sent']['value']) ? number_format($json['net.br-lan']['dimensions']['sent']['value'], 2) . 'KB/s' : 'N/A'; + + $status = 'active'; + $data = [ + 'cpu' => $cpu, + 'memory_free' => $memoryFree, + 'memory_used' => $memoryUsed, + 'load1' => $load1, + 'load5' => $load5, + 'load15' => $load15, + 'disk_in' => $diskIn, + 'disk_out' => $diskOut, + 'network_in' => $networkIn, + 'network_out' => $networkOut + ]; + } else { + $data = [ + 'error' => 'Failed to connect to Netdata. HTTP Status: ' . $response['httpcode'] + ]; + } - return parent::getLiveStats($status, $data); + $visiblestats = []; + if (isset($this->config->availablestats)) { + foreach ($this->config->availablestats as $stat) { + $visiblestats[] = [ + 'title' => self::getAvailableStats()[$stat], + 'value' => $data[$stat] ?? 'N/A' + ]; + } + } + + return parent::getLiveStats($status, ['visiblestats' => $visiblestats]); } public function url($endpoint) { - $api_url = parent::normaliseurl($this->config->url) . $endpoint; - return $api_url; + $config = $this->config; + $url = rtrim($config->url, '/'); + return $url . $endpoint; + } + + public function executeCurl($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $response = curl_exec($ch); + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return [ + 'response' => $response, + 'httpcode' => $httpcode + ]; } } diff --git a/Netdata/config.blade.php b/Netdata/config.blade.php index ce273e941c..35ba2a5877 100644 --- a/Netdata/config.blade.php +++ b/Netdata/config.blade.php @@ -1,9 +1,14 @@

{{ __('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::select('config[availablestats][]', App\SupportedApps\Netdata\Netdata::getAvailableStats(), isset($item) && isset($item->getconfig()->availablestats) ? $item->getconfig()->availablestats : null, ['multiple' => 'multiple', 'class' => 'form-control config-item']) !!} +
diff --git a/Netdata/livestats.blade.php b/Netdata/livestats.blade.php index 3b27dc6745..b8eb263375 100644 --- a/Netdata/livestats.blade.php +++ b/Netdata/livestats.blade.php @@ -1,10 +1,8 @@
    -
  • - Warning - {!! $count_warning !!} -
  • -
  • - Critical - {!! $count_critical !!} -
  • + @foreach ($visiblestats as $stat) +
  • + {{ $stat['title'] }} + {{ $stat['value'] }} +
  • + @endforeach
From fae1d298e98e9fb161efeaf33b1520f3f69b42fe Mon Sep 17 00:00:00 2001 From: GOUKI9999 Date: Fri, 9 Aug 2024 16:10:18 +0800 Subject: [PATCH 4/8] Fix-for-Transmission4 --- Transmission/Transmission.php | 64 +++++++++-------------------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/Transmission/Transmission.php b/Transmission/Transmission.php index 1e93de7a60..0177cd8c8f 100644 --- a/Transmission/Transmission.php +++ b/Transmission/Transmission.php @@ -8,12 +8,10 @@ class Transmission extends \App\SupportedApps implements \App\EnhancedApps public $attrs = []; public $vars; - //protected $login_first = true; // Uncomment if api requests need to be authed first - protected $method = "POST"; // Uncomment if requests to the API should be set by POST + protected $method = "POST"; public function __construct() { - //$this->jar = new \GuzzleHttp\Cookie\CookieJar; // Uncomment if cookies need to be set $body["method"] = "torrent-get"; $body["arguments"] = [ "fields" => ["percentDone", "status", "rateDownload", "rateUpload"], @@ -22,13 +20,13 @@ public function __construct() "http_errors" => false, "timeout" => 5, "body" => json_encode($body), + "verify" => false, ]; } public function test() { $test = $this->sendTest(); - echo $test->status; } @@ -37,18 +35,18 @@ public function livestats() $status = "inactive"; $res = $this->sendRequest(); if ($res == null) { - //Log::debug('Transmission connection failed'); return ""; } $details = json_decode($res->getBody()); if (!isset($details->arguments)) { - //Log::debug('Failed to fetch data from Transmission'); return ""; } $data = []; + error_log(json_encode($details, JSON_PRETTY_PRINT)); + $torrents = $details->arguments->torrents; $seeding_torrents = 0; $leeching_torrents = 0; @@ -69,18 +67,8 @@ public function livestats() $status = "active"; } - $data["download_rate"] = format_bytes( - $rateDownload, - false, - " ", - "/s" - ); - $data["upload_rate"] = format_bytes( - $rateUpload, - false, - " ", - "/s" - ); + $data["download_rate"] = format_bytes($rateDownload, false, " ", "/s"); + $data["upload_rate"] = format_bytes($rateUpload, false, " ", "/s"); $data["seed_count"] = $seeding_torrents; $data["leech_count"] = $leeching_torrents; @@ -90,18 +78,10 @@ public function livestats() private function sendTest() { $this->setClientOptions(); - $test = parent::appTest( - $this->url("transmission/rpc"), - $this->attrs, - $this->vars - ); + $test = parent::appTest($this->url("transmission/rpc"), $this->attrs, $this->vars); if ($test->code === 409) { $this->setClientOptions(); - $test = parent::appTest( - $this->url("transmission/rpc"), - $this->attrs, - $this->vars - ); + $test = parent::appTest($this->url("transmission/rpc"), $this->attrs, $this->vars); } return $test; } @@ -109,18 +89,10 @@ private function sendTest() private function sendRequest() { $this->setClientOptions(); - $res = parent::execute( - $this->url("transmission/rpc"), - $this->attrs, - $this->vars - ); + $res = parent::execute($this->url("transmission/rpc"), $this->attrs, $this->vars); if ($res->getStatusCode() === 409) { $this->setClientOptions(); - $res = parent::execute( - $this->url("transmission/rpc"), - $this->attrs, - $this->vars - ); + $res = parent::execute($this->url("transmission/rpc"), $this->attrs, $this->vars); } return $res; } @@ -134,27 +106,23 @@ private function setClientOptions() $this->config->password, "Basic", ], + "verify" => false, + ]; + } else { + $this->attrs = [ + "verify" => false, ]; } - $res = parent::execute( - $this->url("transmission/rpc"), - $this->attrs, - $this->vars - ); + $res = parent::execute($this->url("transmission/rpc"), $this->attrs, $this->vars); try { - //print_r($res); $xtId = $res->getHeaderLine("X-Transmission-Session-Id"); if ($xtId != null) { $this->attrs["headers"] = [ "X-Transmission-Session-Id" => $xtId, ]; - } else { - //Log::error("Unable to get Transmission session information"); - //Log::debug("Status Code: ".$res->getStatusCode()); } } catch (\GuzzleHttp\Exception\ConnectException $e) { - //Log::error("Failed connection to Transmission"); return false; } return true; From ca36a3df40605441bf7aa0a94686ceb8b2305b80 Mon Sep 17 00:00:00 2001 From: GOUKI9999 Date: Fri, 9 Aug 2024 22:41:30 +0800 Subject: [PATCH 5/8] Some Value Error fix --- Netdata/Netdata.php | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/Netdata/Netdata.php b/Netdata/Netdata.php index 6cab82971d..96342a57fc 100644 --- a/Netdata/Netdata.php +++ b/Netdata/Netdata.php @@ -39,17 +39,40 @@ public function livestats() if ($response['httpcode'] == 200) { $json = json_decode($response['response'], true); - // Extract values and format them - $cpu = isset($json['netdata.plugin_proc_cpu']['dimensions']['user']['value']) ? number_format($json['netdata.plugin_proc_cpu']['dimensions']['user']['value'], 2) . '%' : 'N/A'; - $memoryFree = isset($json['system.ram']['dimensions']['free']['value']) ? number_format($json['system.ram']['dimensions']['free']['value'], 2) . 'MiB' : 'N/A'; - $memoryUsed = isset($json['system.ram']['dimensions']['used']['value']) ? number_format($json['system.ram']['dimensions']['used']['value'], 2) . 'MiB' : 'N/A'; - $load1 = isset($json['system.load']['dimensions']['load1']['value']) ? $json['system.load']['dimensions']['load1']['value'] : 'N/A'; - $load5 = isset($json['system.load']['dimensions']['load5']['value']) ? $json['system.load']['dimensions']['load5']['value'] : 'N/A'; - $load15 = isset($json['system.load']['dimensions']['load15']['value']) ? $json['system.load']['dimensions']['load15']['value'] : 'N/A'; - $diskIn = isset($json['system.io']['dimensions']['in']['value']) ? number_format($json['system.io']['dimensions']['in']['value'], 2) . 'KB/s' : 'N/A'; - $diskOut = isset($json['system.io']['dimensions']['out']['value']) ? number_format($json['system.io']['dimensions']['out']['value'], 2) . 'KB/s' : 'N/A'; - $networkIn = isset($json['net.br-lan']['dimensions']['received']['value']) ? number_format($json['net.br-lan']['dimensions']['received']['value'], 2) . 'KB/s' : 'N/A'; - $networkOut = isset($json['net.br-lan']['dimensions']['sent']['value']) ? number_format($json['net.br-lan']['dimensions']['sent']['value'], 2) . 'KB/s' : 'N/A'; + $cpu = isset($json['system.cpu']['dimensions']['idle']['value']) + ? number_format(100 - $json['system.cpu']['dimensions']['idle']['value'], 1) . '%' + : 'N/A'; + + $memoryFree = isset($json['system.ram']['dimensions']['free']['value']) + ? number_format($json['system.ram']['dimensions']['free']['value'], 1) . 'MB' + : 'N/A'; + $memoryUsed = isset($json['system.ram']['dimensions']['used']['value']) + ? number_format($json['system.ram']['dimensions']['used']['value'], 1) . 'MB' + : 'N/A'; + + $load1 = isset($json['system.load']['dimensions']['load1']['value']) + ? $json['system.load']['dimensions']['load1']['value'] + : 'N/A'; + $load5 = isset($json['system.load']['dimensions']['load5']['value']) + ? $json['system.load']['dimensions']['load5']['value'] + : 'N/A'; + $load15 = isset($json['system.load']['dimensions']['load15']['value']) + ? $json['system.load']['dimensions']['load15']['value'] + : 'N/A'; + + $diskIn = isset($json['system.io']['dimensions']['in']['value']) + ? number_format($json['system.io']['dimensions']['in']['value'], 1) . 'KB/s' + : 'N/A'; + $diskOut = isset($json['system.io']['dimensions']['out']['value']) + ? number_format($json['system.io']['dimensions']['out']['value'], 1) . 'KB/s' + : 'N/A'; + + $networkIn = isset($json['system.net']['dimensions']['InOctets']['value']) + ? number_format($json['system.net']['dimensions']['InOctets']['value'], 1) . 'KB/s' + : 'N/A'; + $networkOut = isset($json['system.net']['dimensions']['OutOctets']['value']) + ? number_format($json['system.net']['dimensions']['OutOctets']['value'], 1) . 'KB/s' + : 'N/A'; $status = 'active'; $data = [ From b5b588c211c21a584a94b5141955d1630eb2392e Mon Sep 17 00:00:00 2001 From: GOUKI9999 Date: Sun, 11 Aug 2024 01:51:15 +0800 Subject: [PATCH 6/8] php-cs-fix --- Transmission/Transmission.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Transmission/Transmission.php b/Transmission/Transmission.php index 0177cd8c8f..59442cf704 100644 --- a/Transmission/Transmission.php +++ b/Transmission/Transmission.php @@ -20,7 +20,7 @@ public function __construct() "http_errors" => false, "timeout" => 5, "body" => json_encode($body), - "verify" => false, + "verify" => false, ]; } From d1b73c1d43131398e96a6289a13bc043e86f23d3 Mon Sep 17 00:00:00 2001 From: GOUKI9999 Date: Sun, 11 Aug 2024 01:52:12 +0800 Subject: [PATCH 7/8] php-cs-fix --- Netdata/Netdata.php | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Netdata/Netdata.php b/Netdata/Netdata.php index 96342a57fc..eab1bff52c 100644 --- a/Netdata/Netdata.php +++ b/Netdata/Netdata.php @@ -39,39 +39,39 @@ public function livestats() if ($response['httpcode'] == 200) { $json = json_decode($response['response'], true); - $cpu = isset($json['system.cpu']['dimensions']['idle']['value']) - ? number_format(100 - $json['system.cpu']['dimensions']['idle']['value'], 1) . '%' + $cpu = isset($json['system.cpu']['dimensions']['idle']['value']) + ? number_format(100 - $json['system.cpu']['dimensions']['idle']['value'], 1) . '%' : 'N/A'; - $memoryFree = isset($json['system.ram']['dimensions']['free']['value']) - ? number_format($json['system.ram']['dimensions']['free']['value'], 1) . 'MB' + $memoryFree = isset($json['system.ram']['dimensions']['free']['value']) + ? number_format($json['system.ram']['dimensions']['free']['value'], 1) . 'MB' : 'N/A'; - $memoryUsed = isset($json['system.ram']['dimensions']['used']['value']) - ? number_format($json['system.ram']['dimensions']['used']['value'], 1) . 'MB' + $memoryUsed = isset($json['system.ram']['dimensions']['used']['value']) + ? number_format($json['system.ram']['dimensions']['used']['value'], 1) . 'MB' : 'N/A'; - $load1 = isset($json['system.load']['dimensions']['load1']['value']) - ? $json['system.load']['dimensions']['load1']['value'] + $load1 = isset($json['system.load']['dimensions']['load1']['value']) + ? $json['system.load']['dimensions']['load1']['value'] : 'N/A'; - $load5 = isset($json['system.load']['dimensions']['load5']['value']) - ? $json['system.load']['dimensions']['load5']['value'] + $load5 = isset($json['system.load']['dimensions']['load5']['value']) + ? $json['system.load']['dimensions']['load5']['value'] : 'N/A'; - $load15 = isset($json['system.load']['dimensions']['load15']['value']) - ? $json['system.load']['dimensions']['load15']['value'] + $load15 = isset($json['system.load']['dimensions']['load15']['value']) + ? $json['system.load']['dimensions']['load15']['value'] : 'N/A'; - $diskIn = isset($json['system.io']['dimensions']['in']['value']) - ? number_format($json['system.io']['dimensions']['in']['value'], 1) . 'KB/s' + $diskIn = isset($json['system.io']['dimensions']['in']['value']) + ? number_format($json['system.io']['dimensions']['in']['value'], 1) . 'KB/s' : 'N/A'; - $diskOut = isset($json['system.io']['dimensions']['out']['value']) - ? number_format($json['system.io']['dimensions']['out']['value'], 1) . 'KB/s' + $diskOut = isset($json['system.io']['dimensions']['out']['value']) + ? number_format($json['system.io']['dimensions']['out']['value'], 1) . 'KB/s' : 'N/A'; - $networkIn = isset($json['system.net']['dimensions']['InOctets']['value']) - ? number_format($json['system.net']['dimensions']['InOctets']['value'], 1) . 'KB/s' + $networkIn = isset($json['system.net']['dimensions']['InOctets']['value']) + ? number_format($json['system.net']['dimensions']['InOctets']['value'], 1) . 'KB/s' : 'N/A'; - $networkOut = isset($json['system.net']['dimensions']['OutOctets']['value']) - ? number_format($json['system.net']['dimensions']['OutOctets']['value'], 1) . 'KB/s' + $networkOut = isset($json['system.net']['dimensions']['OutOctets']['value']) + ? number_format($json['system.net']['dimensions']['OutOctets']['value'], 1) . 'KB/s' : 'N/A'; $status = 'active'; From fdec61d91da027cf84d22d0de7371896baec0f12 Mon Sep 17 00:00:00 2001 From: GOUKI9999 Date: Sun, 17 Aug 2025 12:58:06 +0800 Subject: [PATCH 8/8] WhatsupDocker Enhanced --- WhatsupDocker/WhatsupDocker.php | 149 ++++++++++++++++++++++++++++++ WhatsupDocker/app.json | 11 +++ WhatsupDocker/config.blade.php | 23 +++++ WhatsupDocker/livestats.blade.php | 8 ++ WhatsupDocker/whatsupdocker.png | Bin 0 -> 13371 bytes 5 files changed, 191 insertions(+) create mode 100644 WhatsupDocker/WhatsupDocker.php create mode 100644 WhatsupDocker/app.json create mode 100644 WhatsupDocker/config.blade.php create mode 100644 WhatsupDocker/livestats.blade.php create mode 100644 WhatsupDocker/whatsupdocker.png diff --git a/WhatsupDocker/WhatsupDocker.php b/WhatsupDocker/WhatsupDocker.php new file mode 100644 index 0000000000..c2b05d6195 --- /dev/null +++ b/WhatsupDocker/WhatsupDocker.php @@ -0,0 +1,149 @@ +jar = new \GuzzleHttp\Cookie\CookieJar; // Uncomment if cookies need to be set + } + + public function test() + { + // If auth credentials are provided, test the authenticated endpoint + if ($this->hasAuthCredentials()) { + // Test containers endpoint which requires authentication + $test = parent::appTest($this->url('api/containers'), $this->getAttrs()); + echo $test->status; + } else { + // No auth configured, test the public app endpoint + $test = parent::appTest($this->url('api/app'), $this->getAttrs()); + echo $test->status; + } + } + + public function livestats() + { + $status = 'inactive'; + + try { + // Get watched containers from WUD API + $res = parent::execute($this->url('api/containers'), $this->getAttrs()); + $containers = json_decode($res->getBody(), true); + + if ($containers && is_array($containers)) { + $status = 'active'; + + // Calculate container statistics + $totalContainers = count($containers); + $updatableContainers = 0; + $watchedContainers = 0; + + foreach ($containers as $container) { + // Count updatable containers (based on API doc structure) + if (isset($container['updateAvailable']) && $container['updateAvailable'] === true) { + $updatableContainers++; + } + + // All containers returned by /api/containers are watched containers + $watchedContainers++; + } + + $details = ['visiblestats' => []]; + + // Show configured stats, or all if none selected + $availableStats = isset($this->config->availablestats) && !empty($this->config->availablestats) + ? $this->config->availablestats + : array_keys(self::getAvailableStats()); + + foreach ($availableStats as $stat) { + $newstat = new \stdClass(); + $newstat->title = self::getAvailableStats()[$stat]; + + switch ($stat) { + case 'TotalContainers': + $newstat->value = $totalContainers; + break; + case 'UpdatableContainers': + $newstat->value = $updatableContainers; + break; + case 'WatchedContainers': + $newstat->value = $watchedContainers; + break; + default: + $newstat->value = 0; + } + + $details['visiblestats'][] = $newstat; + } + + return parent::getLiveStats($status, $details); + } + } catch (Exception $e) { + // If auth is configured but containers API fails, don't fallback + if ($this->hasAuthCredentials()) { + $status = 'inactive'; + } else { + // No auth configured, try basic status check with app endpoint + try { + $res = parent::execute($this->url('api/app'), $this->getAttrs()); + if ($res->getStatusCode() === 200) { + $status = 'active'; + } + } catch (Exception $e2) { + $status = 'inactive'; + } + } + } + + return parent::getLiveStats($status, []); + } + + private function hasAuthCredentials() + { + return !empty($this->config->username) && !empty($this->config->password); + } + + private function getAttrs() + { + $attrs = [ + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ] + ]; + + // Add basic authentication if both username and password are configured + if ($this->hasAuthCredentials()) { + $attrs['auth'] = [$this->config->username, $this->config->password]; + } + + return $attrs; + } + + public function url($endpoint) + { + $base_url = parent::normaliseurl($this->config->url); + // Ensure trailing slash for WUD API + if (!str_ends_with($base_url, '/')) { + $base_url .= '/'; + } + return $base_url . $endpoint; + } + + public static function getAvailableStats() + { + return [ + 'TotalContainers' => 'Total', + 'UpdatableContainers' => 'Updatable', + 'WatchedContainers' => 'Watched', + ]; + } +} diff --git a/WhatsupDocker/app.json b/WhatsupDocker/app.json new file mode 100644 index 0000000000..febb1f9261 --- /dev/null +++ b/WhatsupDocker/app.json @@ -0,0 +1,11 @@ +{ + "appid": "471b1748546610893a56b7d9254afa4752f630b9", + "name": "What's up Docker", + "website": "https://github.com/getwud/wud", + "license": "MIT License", + "description": "Gets you notified when new versions of your Docker containers are available and lets you react the way you want.", + "enhanced": true, + "tile_background": "dark", + "icon": "whatsupdocker.png", + "sha": "2f0356bc2cafcca5232e02d1283d2a2bd401ef81" +} \ No newline at end of file diff --git a/WhatsupDocker/config.blade.php b/WhatsupDocker/config.blade.php new file mode 100644 index 0000000000..312067075f --- /dev/null +++ b/WhatsupDocker/config.blade.php @@ -0,0 +1,23 @@ +

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

+
+
+ + {!! Form::text('config[override_url]', null, array('placeholder' => __('app.apps.override'), 'id' => 'override_url', 'class' => 'form-control')) !!} +
+
+ + {!! Form::text('config[username]', null, array('placeholder' => __('app.apps.username'), 'data-config' => 'username', 'class' => 'form-control config-item')) !!} +
+
+ + {!! Form::input('password', 'config[password]', '', ['placeholder' => __('app.apps.password'), 'data-config' => 'password', 'class' => 'form-control config-item']) !!} +
+
+ + {!! Form::select('config[availablestats][]', \App\SupportedApps\WhatsupDocker\WhatsupDocker::getAvailableStats(), isset($item) ? $item->getConfig()->availablestats ?? null : null, ['multiple' => 'multiple']) !!} +
+
+ +
+
+ diff --git a/WhatsupDocker/livestats.blade.php b/WhatsupDocker/livestats.blade.php new file mode 100644 index 0000000000..66e281342d --- /dev/null +++ b/WhatsupDocker/livestats.blade.php @@ -0,0 +1,8 @@ +
    + @foreach ($visiblestats as $stat) +
  • + {!! $stat->title !!} + {!! $stat->value !!} +
  • + @endforeach +
\ No newline at end of file diff --git a/WhatsupDocker/whatsupdocker.png b/WhatsupDocker/whatsupdocker.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a5cf70ce1eef398a6c5ae7fd475fba3392efdb GIT binary patch literal 13371 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