mirror of
https://github.com/daledavies/jump.git
synced 2026-02-25 15:50:45 +01:00
Add site status detection feature
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
// Variables for status colours
|
||||
$online-color: #55e22a;
|
||||
$offline-color: #ec2c2c;
|
||||
$error-color: #ddd900;
|
||||
$unknown-color: #ccc;
|
||||
|
||||
.sites {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -32,6 +38,7 @@
|
||||
background-color: #fff;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
border: .2em solid #fff;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,.3);
|
||||
@@ -60,6 +67,66 @@
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
.sites:not(.alternate) {
|
||||
.icon {
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
height: 82px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-color: $unknown-color;
|
||||
}
|
||||
}
|
||||
.online .icon::after {
|
||||
background-color: $online-color;
|
||||
}
|
||||
.offline .icon::after {
|
||||
background-color: $offline-color;
|
||||
}
|
||||
.error .icon::after {
|
||||
background-color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
.sites.alternate {
|
||||
li {
|
||||
a {
|
||||
padding-left: 11px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: $unknown-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.online a::before {
|
||||
background-color: $online-color;
|
||||
}
|
||||
&.offline a::before {
|
||||
background-color: $offline-color;
|
||||
}
|
||||
&.error a::before {
|
||||
background-color: $error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sites.alternate {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
|
||||
@@ -50,9 +50,9 @@ export default class Main {
|
||||
|
||||
if (this.showsearchbuttonelm) {
|
||||
this.searchclosebuttonelm = this.showsearchbuttonelm.querySelector('.close');
|
||||
this.fuse = new Fuse(JSON.parse(JUMP.search), {
|
||||
this.fuse = new Fuse(JSON.parse(JUMP.sites), {
|
||||
threshold: 0.3,
|
||||
keys: ['name', 'tags']
|
||||
keys: ['name', 'tags', 'url']
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,24 @@ export default class Main {
|
||||
});
|
||||
}
|
||||
|
||||
// If enables then check the status API and update the frontend according to result.
|
||||
if (JUMP.checkstatus) {
|
||||
fetch(JUMP.wwwurl + '/api/status/' + JUMP.token + '/')
|
||||
.then(response => response.json())
|
||||
.then(statuses => {
|
||||
for (const [id, status] of Object.entries(statuses)) {
|
||||
const siteelm = document.querySelector('#'+id);
|
||||
if (siteelm) {
|
||||
siteelm.classList.add(status);
|
||||
if (status !== 'online') {
|
||||
const sitelinkelm = siteelm.querySelector('a');
|
||||
sitelinkelm.title = '(Status: ' + status + ') ' + sitelinkelm.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start listening for events so we can do stuff when needed.
|
||||
this.add_event_listeners();
|
||||
// If there is no OWM API key provided then just update the greeting
|
||||
|
||||
29
jumpapp/classes/API/Status.php
Normal file
29
jumpapp/classes/API/Status.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* ██ ██ ██ ███ ███ ██████
|
||||
* ██ ██ ██ ████ ████ ██ ██
|
||||
* ██ ██ ██ ██ ████ ██ ██████
|
||||
* ██ ██ ██ ██ ██ ██ ██ ██
|
||||
* █████ ██████ ██ ██ ██
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @copyright Copyright (c) 2022, Dale Davies
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Jump\API;
|
||||
|
||||
class Status extends AbstractAPI {
|
||||
|
||||
public function get_output(): string {
|
||||
$this->validate_token();
|
||||
$statusarray = [];
|
||||
$sites = (new \Jump\Sites($this->config, $this->cache))->get_sites();
|
||||
foreach ($sites as $site) {
|
||||
$status = $site->get_status();
|
||||
$statusarray[$site->id] = $status;
|
||||
}
|
||||
return json_encode($statusarray);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,7 +24,7 @@ use Nette\Caching;
|
||||
*/
|
||||
class Cache {
|
||||
|
||||
private Caching\Storages\FileStorage $storage;
|
||||
private Caching\Storages\FileStorage|Caching\Storages\DevNullStorage $storage;
|
||||
|
||||
/**
|
||||
* The definition of various caches used throughout the application.
|
||||
@@ -33,10 +33,12 @@ class Cache {
|
||||
*/
|
||||
private array $caches;
|
||||
|
||||
/**
|
||||
* Creates file storage for cache and initialises cache objects for each
|
||||
* name/type specified in $caches definition.
|
||||
*/
|
||||
/**
|
||||
* Creates file storage for cache and initialises cache objects for each
|
||||
* name/type specified in $caches definition.
|
||||
*
|
||||
* @param Config $config
|
||||
*/
|
||||
public function __construct(private Config $config) {
|
||||
// Define the various caches used throughout the app.
|
||||
$this->caches = [
|
||||
@@ -50,6 +52,11 @@ class Cache {
|
||||
'expirationtype' => Caching\Cache::FILES,
|
||||
'expirationparams' => $config->get('sitesfile')
|
||||
],
|
||||
'sites/status' => [
|
||||
'cache' => null,
|
||||
'expirationtype' => Caching\Cache::EXPIRE,
|
||||
'expirationparams' => '5 minutes'
|
||||
],
|
||||
'tags' => [
|
||||
'cache' => null,
|
||||
'expirationtype' => Caching\Cache::FILES,
|
||||
@@ -85,7 +92,12 @@ class Cache {
|
||||
],
|
||||
];
|
||||
// Inititalise file storage for cache using cachedir path from config.
|
||||
$this->storage = new Caching\Storages\FileStorage($this->config->get('cachedir').'/application');
|
||||
// If cachebypass has been set in config.php then use DevNullStorage instead.
|
||||
if ($this->config->parse_bool($this->config->get('cachebypass'))) {
|
||||
$this->storage = new Caching\Storages\DevNullStorage();
|
||||
} else {
|
||||
$this->storage = new Caching\Storages\FileStorage($this->config->get('cachedir').'/application');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,10 +128,6 @@ class Cache {
|
||||
* @return mixed The result of callback function retreieved from cache.
|
||||
*/
|
||||
public function load(string $cachename, ?string $key = 'default', callable $callback = null): mixed {
|
||||
// If cachebypass has been set in config.php then just execute the callback.
|
||||
if ($this->config->parse_bool($this->config->get('cachebypass')) && $callback !== null) {
|
||||
return $callback();
|
||||
}
|
||||
$this->init_cache($cachename, $key);
|
||||
// Retrieve the initialised cache object from $caches.
|
||||
if ($callback === null) {
|
||||
@@ -143,10 +151,9 @@ class Cache {
|
||||
* @param mixed $data
|
||||
* @return void
|
||||
*/
|
||||
public function save(string $cachename, ?string $key = 'default', mixed $data) {
|
||||
public function save(string $cachename, ?string $key = 'default', mixed $data): mixed {
|
||||
$this->init_cache($cachename, $key);
|
||||
$dependencies = [$this->caches[$cachename]['expirationtype'] => $this->caches[$cachename]['expirationparams']];
|
||||
// Retrieve the initialised cache object from $caches.
|
||||
return $this->caches[$cachename]['cache'][$key]->save($cachename.'/'.$key, $data, $dependencies);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ class Main {
|
||||
$this->router->addRoute('/api/icon?siteurl=<siteurl>', [
|
||||
'class' => 'Jump\API\Icon'
|
||||
]);
|
||||
$this->router->addRoute('/api/status[/<token>]', [
|
||||
'class' => 'Jump\API\Status'
|
||||
]);
|
||||
$this->router->addRoute('/api/unsplash[/<token>]', [
|
||||
'class' => 'Jump\API\Unsplash'
|
||||
]);
|
||||
|
||||
@@ -34,12 +34,15 @@ class HomePage extends AbstractPage {
|
||||
'unsplash' => !!$this->config->get('unsplashapikey', false),
|
||||
'unsplashcolor' => $unsplashdata?->color,
|
||||
'wwwurl' => $this->config->get_wwwurl(),
|
||||
'checkstatus' => $this->config->parse_bool($this->config->get('checkstatus', false)),
|
||||
];
|
||||
if ($this->config->parse_bool($this->config->get('showsearch', false))) {
|
||||
$templatecontext = array_merge($templatecontext, [
|
||||
'searchengines' => json_encode((new \Jump\SearchEngines($this->config, $this->cache))->get_search_engines()),
|
||||
'searchjson' => json_encode((new \Jump\Sites($this->config, $this->cache))->get_sites_for_search()),
|
||||
]);
|
||||
$showsearch = $this->config->parse_bool($this->config->get('showsearch', false));
|
||||
$showstatus = $this->config->parse_bool($this->config->get('showsearch', false));
|
||||
if ($showsearch || $showstatus) {
|
||||
$templatecontext['sitesjson'] = json_encode((new \Jump\Sites($this->config, $this->cache))->get_sites_for_frontend());
|
||||
if ($showsearch) {
|
||||
$templatecontext['searchengines'] = json_encode((new \Jump\SearchEngines($this->config, $this->cache))->get_search_engines());
|
||||
}
|
||||
}
|
||||
return $template->render($templatecontext);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class Site {
|
||||
if (!isset($sitearray['name'], $sitearray['url'])) {
|
||||
throw new \Exception('The array passed to Site() must contain the keys "name" and "url"!');
|
||||
}
|
||||
$this->id = preg_replace("/[^A-Za-z0-9 ]/", '', $sitearray['url']);
|
||||
$this->name = $sitearray['name'];
|
||||
$this->url = $sitearray['url'];
|
||||
$this->nofollow = isset($sitearray['nofollow']) ? $sitearray['nofollow'] : (isset($this->defaults['nofollow']) ? $this->defaults['nofollow'] : false);
|
||||
@@ -91,4 +92,13 @@ class Site {
|
||||
return 'data:'.$imagedata->mimetype.';base64,'.base64_encode($imagedata->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the online status of this site.
|
||||
*
|
||||
* @return string The site status.
|
||||
*/
|
||||
public function get_status(): string {
|
||||
$cache = new Cache($this->config);
|
||||
return (new Status($cache, $this))->get_status();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,21 @@ class Sites {
|
||||
return $this->loadedsites[$found];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a Site ID, does that site exist in our list of sites?
|
||||
*
|
||||
* @param string $id The Site ID to search for.
|
||||
* @return Site A matching Site object if found.
|
||||
* @throws Exception If a site with given Site ID does not exist.
|
||||
*/
|
||||
public function get_site_by_id(string $id): Site {
|
||||
$found = array_search($id, array_column($this->get_sites(), 'id'));
|
||||
if ($found === false) {
|
||||
throw new Exception('The site could not be found ('.$id.')');
|
||||
}
|
||||
return $this->loadedsites[$found];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Site objects with a given tag.
|
||||
*
|
||||
@@ -162,14 +177,22 @@ class Sites {
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function get_sites_for_search(): array {
|
||||
/**
|
||||
* Get a list of cached sites from for use in the front end via JS. Some extra details
|
||||
* added to each site in the list may not be already saved in the main sites cache.
|
||||
*
|
||||
* @return array Array of stdClass objects containing required site details.
|
||||
*/
|
||||
public function get_sites_for_frontend(): array {
|
||||
$searchlist = [];
|
||||
foreach ($this->loadedsites as $loadedsite) {
|
||||
$site = new \stdClass();
|
||||
$site->id = $loadedsite->id;
|
||||
$site->name = $loadedsite->name;
|
||||
$site->url = $loadedsite->url;
|
||||
$site->tags = $loadedsite->tags;
|
||||
$site->iconurl = '/api/icon?siteurl='.urlencode($loadedsite->url);
|
||||
$site->status = $this->cache->load(cachename: 'sites/status', key: $site->url) ?? null;
|
||||
$searchlist[] = $site;
|
||||
}
|
||||
return $searchlist;
|
||||
|
||||
72
jumpapp/classes/Status.php
Normal file
72
jumpapp/classes/Status.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* ██ ██ ██ ███ ███ ██████
|
||||
* ██ ██ ██ ████ ████ ██ ██
|
||||
* ██ ██ ██ ██ ████ ██ ██████
|
||||
* ██ ██ ██ ██ ██ ██ ██ ██
|
||||
* █████ ██████ ██ ██ ██
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @copyright Copyright (c) 2022, Dale Davies
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Jump;
|
||||
|
||||
class Status {
|
||||
|
||||
private const STATUS_UNKNOWN = 'unknown';
|
||||
private const STATUS_ONLINE = 'online';
|
||||
private const STATUS_OFFLINE = 'offline';
|
||||
private const STATUS_ERROR = 'error';
|
||||
|
||||
private $connectionTimeout = 10;
|
||||
private $requestTimeout = 30;
|
||||
|
||||
/**
|
||||
* Allows for checking if a site is online/offline or returns an error code.
|
||||
*
|
||||
* @param Cache $cache
|
||||
* @param Site $site
|
||||
*/
|
||||
public function __construct(private Cache $cache, public Site $site) {
|
||||
$this->status = $this->cache->load(cachename: 'sites/status', key: $this->site->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site's status.
|
||||
*
|
||||
* @return string The site status.
|
||||
*/
|
||||
public function get_status(): string {
|
||||
// If we haven't got a status already cachhed then try connecting to the site
|
||||
// and save the status to the cache.
|
||||
if (!$this->status) {
|
||||
// Create a new client with client config.
|
||||
$client = new \GuzzleHttp\Client([
|
||||
'connect_timeout' => $this->connectionTimeout,
|
||||
'timeout' => $this->requestTimeout,
|
||||
'allow_redirects' => true
|
||||
]);
|
||||
// Try to connect to site and determine status.
|
||||
try {
|
||||
if($client->request('GET', $this->site->url)) {
|
||||
$status = self::STATUS_ONLINE;
|
||||
}
|
||||
} catch (\GuzzleHttp\Exception\ConnectException) {
|
||||
// Catch instances where we cant connect.
|
||||
$status = self::STATUS_OFFLINE;
|
||||
} catch (\GuzzleHttp\Exception\BadResponseException) {
|
||||
// Catch 4xx and 5xx errors.
|
||||
$status = self::STATUS_ERROR;
|
||||
} catch (\Exception) {
|
||||
// If anything went wrong or we had some other status code.
|
||||
$status = self::STATUS_UNKNOWN;
|
||||
}
|
||||
// Save the status to the cache.
|
||||
$this->status = $this->cache->save(cachename: 'sites/status', key: $this->site->id, data: $status);
|
||||
}
|
||||
// Finally return the status.
|
||||
return $this->status;
|
||||
}
|
||||
}
|
||||
@@ -57,4 +57,6 @@ return [
|
||||
'latlong' => getenv('LATLONG') ?: '',
|
||||
// Temperature unit: True = metric / False = imperial.
|
||||
'metrictemp' => getenv('METRICTEMP') ?: true,
|
||||
|
||||
'checkstatus' => getenv('CHECKSTATUS') ?: true,
|
||||
];
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
metrictemp: '{{metrictemp}}',
|
||||
ampmclock: '{{ampmclock}}',
|
||||
token: '{{csrftoken}}',
|
||||
search: '{{{searchjson}}}',
|
||||
sites: '{{{sitesjson}}}',
|
||||
checkstatus: '{{checkstatus}}',
|
||||
searchengines: '{{{searchengines}}}',
|
||||
unsplash: '{{{unsplash}}}',
|
||||
unsplashcolor: '{{unsplashcolor}}',
|
||||
@@ -25,7 +26,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content fixed hidden">
|
||||
<div class="content fixed hidden {{# checkstatus}}status{{/ checkstatus}}">
|
||||
<div class="greeting">
|
||||
{{# greeting}}<span class="tagname"><span>#</span>{{greeting}}</span>{{/ greeting}}
|
||||
{{^ greeting}}Good <span class="chosen"></span>{{/ greeting}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{# hassites}}
|
||||
<ul class="sites {{# altlayout}}alternate{{/ altlayout}}">
|
||||
{{# sites}}<li><a {{# newtab}}target="_blank"{{/ newtab}} rel="{{# nofollow}}nofollow {{/ nofollow}}{{# newtab}}noopener{{/ newtab}}" title="{{description}}" href="{{url}}">
|
||||
{{# sites}}<li id="{{id}}"><a {{# newtab}}target="_blank"{{/ newtab}} rel="{{# nofollow}}nofollow {{/ nofollow}}{{# newtab}}noopener{{/ newtab}}" title="{{description}}" href="{{url}}">
|
||||
<span class="icon">
|
||||
<img src="{{{wwwurl}}}/api/icon?siteurl={{#urlencode}}{{url}}{{/urlencode}}">
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user