Compare commits

...
Sign in to create a new pull request.

83 commits

Author SHA1 Message Date
28f1bcfce9 Changing frequency 2022-01-10 20:34:23 +01:00
9fe028e1cc Removing old version of Monitolite 2022-01-06 16:14:19 +01:00
ec4fb68630 Revert "Removing old version of Monitolite"
This reverts commit 42fe9281f7.
2022-01-06 16:12:51 +01:00
42fe9281f7 Removing old version of Monitolite 2022-01-06 16:10:22 +01:00
3daa19f2de Building as production 2021-12-29 11:32:44 +01:00
25f5a8800d Fixing API request 2021-12-29 11:32:01 +01:00
1414c83cc9 Improving query 2021-12-28 20:22:22 +01:00
78dc7baa73 Adding task type 2021-12-28 18:37:37 +01:00
83d401daa2 Adding missing icon 2021-12-28 18:31:56 +01:00
1996b4fb32 Updating README for DNS 2021-12-28 18:25:57 +01:00
5986875818 Adding DNS check 2021-12-28 18:24:40 +01:00
80ba45b83b README 2021-12-28 17:44:16 +01:00
f2139a7b01 Modifying the README 2021-12-28 17:43:32 +01:00
42f1187776 Adding FTP (anonymous) check 2021-12-28 17:41:52 +01:00
dde679411a Fixing relative URL 2021-12-28 16:37:48 +01:00
f1e1cbfdd9 Adding message when no data 2021-12-28 15:19:47 +01:00
d9c2c6f5f6 Fixing case when task not found 2021-12-28 14:04:44 +01:00
24ed50d9d2 Moving to migrations 2021-12-28 13:34:50 +01:00
fc6f36ce3a Rounding values 2021-12-28 12:52:02 +01:00
fccf7bf165 Merge branch 'main' of github.com:axeloz/monitolite into main 2021-12-28 12:50:34 +01:00
e89e4ca451 Fixing graphs values 2021-12-28 12:50:26 +01:00
6ba0d76dda
Update README.md 2021-12-28 10:52:33 +01:00
230e1c5668
Update README.md 2021-12-28 10:51:59 +01:00
5b73ad72e4 Merge branch 'main' of github.com:axeloz/monitolite into main 2021-12-28 10:48:03 +01:00
d292b0342e Latest db schema 2021-12-28 10:47:54 +01:00
d3f05d6ee1
Update README.md 2021-12-28 10:33:08 +01:00
b0e28950bc Fixing example .env 2021-12-28 10:26:10 +01:00
6fe20c0102 New screenshot 2021-12-28 10:04:37 +01:00
ea63595795 Do not display response time 2021-12-28 09:29:24 +01:00
a704c5bea3 Do not display response times for ping checks 2021-12-28 09:27:57 +01:00
d57cee575b Removing deletion of missing customers 2021-12-28 09:13:01 +01:00
a71505def5 Fixing average 2021-12-28 00:09:56 +01:00
a9ee94b8fd Restoring loader 2021-12-28 00:07:29 +01:00
234547a903 Changing color 2021-12-28 00:06:23 +01:00
84ec4847d9 Adding gradiant 2021-12-27 23:59:25 +01:00
f20adb7f28 Fixing history 2021-12-27 23:37:15 +01:00
3852ef2945 Fixing charts 2021-12-27 23:26:18 +01:00
bae507328b Adding response time graph 2021-12-26 19:03:13 +01:00
2c415cd08b Message when no duration 2021-12-24 10:34:30 +01:00
441353fcb0 Forgot to add output 2021-12-24 10:32:56 +01:00
9ead771d6b Keeping track of duration 2021-12-24 10:23:04 +01:00
1e0d78f67b Changing README 2021-12-24 10:03:31 +01:00
73dd738970 Fixing loader 2021-12-24 09:53:53 +01:00
6ca118f717 Adding loader 2021-12-24 09:45:18 +01:00
e344d5ca38 New db schema 2021-12-24 09:45:07 +01:00
0e7221da07 Improved request 2021-12-24 09:41:47 +01:00
c1817425cd Adding loader 2021-12-23 22:42:26 +01:00
8d7bd04424 Adding notifications log 2021-12-23 19:55:11 +01:00
afd273081b Fix dates 2021-12-23 19:07:59 +01:00
9f4de0dee6 Fixing graph 2021-12-23 18:43:52 +01:00
bbc1afbddc Fixing details 2021-12-23 17:46:02 +01:00
387d910e44 Better styling 2021-12-23 16:55:10 +01:00
3981724c86 Adding date selector 2021-12-23 16:42:48 +01:00
4067a8448b More advanced task details with graph ! 2021-12-23 16:19:48 +01:00
a78320344f Fix 2021-12-23 12:21:53 +01:00
646e7de68f Starting task details 2021-12-23 12:20:55 +01:00
c832ca94fa Removing debug 2021-12-22 17:14:02 +01:00
a997dec6a5 Cleaning unused imports 2021-12-22 16:19:00 +01:00
6a9b886796 Renaming commands 2021-12-22 16:18:19 +01:00
327b2d8f63 Adding email notifications 2021-12-22 16:17:44 +01:00
30dba8447f Temporary fix 2021-12-21 21:58:43 +01:00
22b6b9de57 Adding message when there is no task 2021-12-21 21:48:23 +01:00
e48daa0008 Fixing customers 2021-12-21 20:33:42 +01:00
630ac562ee Fixing customers 2021-12-21 20:33:16 +01:00
c2e2532103 Fixing at last 2021-12-21 19:55:14 +01:00
a518468434 Fixing bug 2021-12-21 19:54:42 +01:00
a1ee1d0335 Fixing bug 2021-12-21 19:54:04 +01:00
9c170504cd FIxing bug 2021-12-21 19:53:30 +01:00
416f5294e6 Full rewrite Laravel style 2021-12-21 19:44:56 +01:00
cc03a0cf03 Starting migrations 2021-12-21 10:18:58 +01:00
b163ce6c5c Removing dd 2021-12-21 09:21:27 +01:00
4381db999c Fixing weird LUMEN timezone for DB 2021-12-20 23:54:03 +01:00
81bcaed76d Increasing refresh time 2021-12-20 23:17:16 +01:00
217428296a Fixing text search 2021-12-20 23:15:51 +01:00
7b999e994b Fixing time 2021-12-20 22:59:08 +01:00
4a36d03729 Fixing time 2021-12-20 22:58:37 +01:00
68d37f6db8 Fixing time 2021-12-20 22:57:45 +01:00
82f543a587 Fixing MySQL error 2021-12-20 22:30:05 +01:00
1cc4ac9a95 ?? 2021-12-20 22:19:44 +01:00
696d7fd299 Trying to bugfix 2021-12-20 22:17:07 +01:00
746e5afa8f Using query builder 2021-12-20 22:14:49 +01:00
caa8ca8ce5 Fixing typo 2021-12-20 21:54:06 +01:00
3a288ff15f Removing CleanHistory 2021-12-20 21:43:46 +01:00
53 changed files with 2872 additions and 3435 deletions

View file

@ -1,9 +1,10 @@
APP_NAME=Monitolite
APP_ENV=local
APP_ENV=production
APP_KEY=
APP_DEBUG=true
APP_DEBUG=false
APP_URL=http://localhost
APP_TIMEZONE=UTC
DB_TIMEZONE="+1:00"
LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=
@ -18,11 +19,14 @@ DB_PASSWORD=secret
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SMTP_HOST=localhost
SMTP_USER=
SMTP_PASSWORD=
SMTP_PORT=25
SMTP_SSL=1
MAIL_FROM=axel@monitolite.fr
NB_TRIES=3
ARCHIVE_DAYS=10
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@monitolite.fr
MAIL_FROM_NAME="Monitolite"

View file

@ -7,8 +7,10 @@ I figured it could be useful for others so I **rewrote** and **updated** it from
## What it does
**MonitoLite** is a very simple monitoring tool developed in PHP powered by Lumen (by Laravel). It supports :
* **ping monitoring**: sends a `ping` command to the specified host. Raises an alert if the host is down
* **http monitoring**: requests the provided URL and raises an alert if the URL returns an error. Optionally you may specify a string to search on the page using the `param` database field. It raises an alert if the specified text could not be found on the page.
* **PING monitoring**: sends a `ping` command to the specified host. Raises an alert if the host is down
* **HTTP monitoring**: requests the provided URL and raises an alert if the URL returns an error. Optionally you may specify a string to search on the page using the `param` database field. It raises an alert if the specified text could not be found on the page.
* **FTP monitoring**: connects to the provided FTP server as anonymous (authentication not supported yet).
* **DNS monitoring**: runs a DNS lookup on a given DNS server for the hostname specified in the params
In case of an alert, the script sends an email notifications to the specified contacts (one or many).
The script also sends a recovery email notification when the alert is over.
@ -19,10 +21,20 @@ Tested on MySQL only but should support other SQL-based DBMS.
It comes with a very straightforward dashboard written in PHP. This is **optional**, the monitoring script runs as standalone.
**Caution**: the backend is not password-protected. You should make sure you add your own security layer via IP filtering or basic authentication.
## Demo
[DEMO](https://monitolite.mabox.eu)
## Screenshot
### Tasks list with quick preview
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot.png "Logo")
### Task details with graph and history
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot2.png "Logo")
## Requirements
@ -36,43 +48,51 @@ It comes with a very straightforward dashboard written in PHP. This is **optiona
* clone this repo
* install PHP composer dependencies: `cd ./web && composer install`
* create a Database and import the schema from `sql/create.sql`
* create your own `.env` file: `cp env.example .env` and adapt it to your needs
* create a Database and import the initial schema using `php artisan migrate`
* create your own `.env` file: `cp .env.example .env` and adapt it to your needs
* create a webserver vhost with document root to the `public` directory
* add tasks and contacts into the database (no backend yet)
* run the script: `cd /var/www/<your-path> && php artisan monitolite:monitoring:run`
* add tasks and contacts into the database (no GUI for CRUD yet)
* run the script: `cd /var/www/<your-path> && php artisan monitolite:run`
* check the output of the command for results.
* if everything works, you may create a CRON `* * * * * cd /var/www/<your-path> && php artisan monitolite:monitoring:run > /dev/null`
* if everything works, you may create a CRON `* * * * * cd /var/www/<your-path> && php artisan monitolite:run > /dev/null`
## Settings
* DB_TYPE=mysql
* APP_NAME=Monitolite
* APP_ENV=production
* APP_KEY=<GENERATE KEY HERE>
* APP_DEBUG=false
* APP_URL=http://localhost
* APP_TIMEZONE=UTC
* DB_TIMEZONE="+1:00"
* DB_CONNECTION=mysql
* DB_HOST=127.0.0.1
* DB_USER=vagrant
* DB_PASSWORD=vagrant
* DB_NAME=monitoring
* DB_PORT=3306
* SMTP_HOST=localhost
* SMTP_USER=
* SMTP_PASSWORD=
* SMTP_PORT=80
* SMTP_SSL=1
* MAIL_FROM=axel@monitolite.fr
* DB_DATABASE=homestead
* DB_USERNAME=homestead
* DB_PASSWORD=secret
* MAIL_MAILER=smtp
* MAIL_HOST=localhost
* MAIL_PORT=25
* MAIL_USERNAME=
* MAIL_PASSWORD=
* MAIL_ENCRYPTION=
* MAIL_FROM_ADDRESS=noreply@monitolite.fr
* MAIL_FROM_NAME="Monitolite"
* NB_TRIES=3
* ARCHIVE_DAYS=10
## MORE INFORMATION COMING SOON.
## TODO
* Make CRUD possible from the backend for adding tasks and contacts
* Multithreading
* SMS Notifications
* Protected backend with authentication
* Create an installation script
* Raise alert when tasks are not run at the correct frequency (CRON down or other reason)
* Set a notification capping limit to prevent many notifications to be sent in case of an up-and-down host
* Add a notification history log
* Keep track of tasks response time
* Daemonize the script (instead of CRONs)
[ ] Make CRUD possible from the backend for adding tasks and contacts
[ ] Multithreading
[ ] SMS Notifications
[ ] Protected backend with authentication
[ ] Create an installation script
[ ] Raise alert when tasks are not run at the correct frequency (CRON down or other reason)
[x] Set a notification capping limit to prevent many notifications to be sent in case of an up-and-down host
[x] Add a notification history log
[x] Keep track of tasks response time
[ ] Daemonize the script (instead of CRONs)

View file

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CleanHistory extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitolite:purge';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Aggregates and cleans tasks history';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$lastweek = \Carbon\Carbon::now()->subWeek();
$history = app('db')->select('
SELECT * FROM task_history as h
WHERE created_at < :lastweek
', [
'lastweek' => $lastweek
]);
}
}

View file

@ -2,21 +2,27 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use \Exception;
use Illuminate\Queue\Console\MonitorCommand;
use App\Models\Task;
use App\Models\TaskHistory;
use App\Models\Notification;
use Illuminate\Console\Command;
class RunMonitoring extends Command
{
private $rounds = 50;
private $limit = 50;
private $max_tries = 3;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitolite:monitoring:run {rounds?}';
protected $signature = 'monitolite:run
{--limit=50 : the number of tasks to handle in one run}
{--task= : the ID of an individual task to handle}
{--force : handles tasks even if they are pending}
';
/**
* The console command description.
@ -48,68 +54,127 @@ class RunMonitoring extends Command
public function handle()
{
$count = 0;
$rounds = $this->argument('rounds') ?? $this->rounds;
$limit = $this->option('limit') ?? $this->limit;
$this->max_tries = env('NB_TRIES', $this->max_tries);
// If a force has been asked via command line
$force = false;
if (! empty($this->option('force'))) {
if (empty($this->option('task'))) {
if ($this->confirm('You asked me to force the execution (--force) but you did not specify a particular task ID (--task). I might have to handle a large amount of tasks. Are you sure?')) {
$force = true;
}
}
else {
$force = true;
}
}
// Getting pending tasks
$tasks = app('db')->select('
SELECT id, host, type, params
FROM tasks
WHERE ( DATE_SUB(now(), INTERVAL frequency SECOND) > last_execution OR last_execution IS NULL )
AND active = 1
ORDER BY last_execution ASC
LIMIT :limit
', [
'limit' => $rounds
]);
$tasks = Task::where(function($query) use ($force) {
$query->whereRaw('DATE_SUB(NOW(), INTERVAL frequency SECOND) > executed_at');
$query->orWhereBetween('attempts', [1, ($this->max_tries - 1)]);
$query->orWhereNull('executed_at');
if ($force === true) {
$query->orWhere('id', '>', 0);
}
})
->where('active', 1)
->orderBy('attempts', 'DESC')
->orderBy('executed_at', 'ASC')
->take($limit)
;
// If a particular task has been set via the command line
if (! empty($this->option('task'))) {
$tasks = $tasks->where('id', '=', $this->option('task'));
}
// Now getting tasks
$tasks = $tasks->get();
if (is_null($tasks) || count($tasks) == 0) {
$this->info('No task to process, going back to sleep');
return true;
}
$this->info('I have '.count($tasks).' to process. Better get started ...');
$this->info('I have '.count($tasks).' tasks to process. Better get started ...');
$this->newLine();
$bar = $this->output->createProgressBar(count($tasks));
$bar->start();
foreach ($tasks as $task) {
$last_status = $new_status = $output = null;
$bar->advance();
// Getting current task last status
$query = DB::table('tasks_history')
->select('status')
->where('task_id', $task->id)
->orderBy('datetime', 'DESC')
->first()
;
if ($query !== false && ! is_null($query)) {
$last_status = $query->status;
}
$previous_status = $task->status;
try {
switch ($task->type) {
case 'ping':
$new_status = $this->checkPing($task);
$result = $this->checkPing($task);
break;
case 'http':
$new_status = $this->checkHttp($task);
$result = $this->checkRequest($task, CURLPROTO_HTTP | CURLPROTO_HTTPS);
break;
case 'ftp':
$result = $this->checkRequest($task, CURLPROTO_FTP | CURLPROTO_FTPS);
break;
case 'dns':
$result = $this->checkDns($task);
break;
default:
// Nothing to do here
continue 2;
throw new Exception('Unknown type "'.$task->type.'"');
}
$this->saveHistory($task, true);
$new_status = 1;
$history = $this->saveHistory($task, true, 'success', $result['duration'] ?? null);
}
catch(MonitoringException $e) {
$this->saveHistory($task, false, $e->getMessage());
$history = $this->saveHistory($task, false, $e->getMessage());
}
catch(Exception $e) {
$this->saveHistory($task, false, $e->getMessage());
//TODO: handle system exception differently
//$history = $this->saveHistory($task, false, $e->getMessage());
$this->error($e->getMessage());
}
finally {
// Changing task timestamps and status
$task->executed_at = $history->created_at; # Using the same timestamp as the task history
$task->attempts = $history->status == 1 ? 0 : $task->attempts + 1; # when success, resetting counter
/**
* We don't want to change the primary status in the task table
* as long as failed tasks have reached the max tries limit
* In the cast of a success, we can change the status straight away
*/
if ($history->status == 0 && $task->attempts >= $this->max_tries) {
$task->status = 0;
}
else if ($history->status === 1) {
$task->status = 1;
}
if (! $task->save()) {
throw new Exception('Cannot save task details');
}
// Task status has changed
// But not from null (new task)
if (! is_null($previous_status) && $task->status != $previous_status) {
// If host is up, no double-check
if ($task->status == 1 || ($task->status == 0 && $task->attempts == $this->max_tries)) {
Notification::addNotificationTask($history);
}
}
}
}
$bar->finish();
@ -117,42 +182,40 @@ class RunMonitoring extends Command
if (!empty($this->results)) {
$this->table(
['Host', 'Result', 'Message'],
['ID', 'Host', 'Type', 'Result', 'Attempts', 'Message'],
$this->results
);
}
}
final private function saveHistory($task, $status, $output = null) {
final private function saveHistory(Task $task, $status, $output = null, $duration = null) {
$date = date('Y-m-d H:i:s');
// Inserting new history
$insert = new TaskHistory;
$insert->status = $status === true ? 1 : 0;
$insert->created_at = $date;
$insert->output = $output ?? '';
$insert->duration = $duration;
$insert->task_id = $task->id;
if (! $insert->save()) {
throw new Exception('Cannot insert history for task #'.$task->id);
}
$this->results[] = [
'id' => $task->id,
'host' => $task->host,
'type' => $task->type,
'result' => $status === true ? 'OK' : 'FAILED',
'attempts' => $task->attempts,
'message' => $output
];
$insert = DB::table('tasks_history')
->insert([
'status' => $status === true ? 1 : 0,
'datetime' => $date,
'output' => $output ?? '',
'task_id' => $task->id
]
);
if (false !== $insert) {
DB::table('tasks')
->where('id', $task->id)
->update([
'last_execution' => $date
])
;
return true;
}
return $insert;
}
final private function checkPing($task) {
final private function checkPing(Task $task) {
if (! function_exists('exec') || ! is_callable('exec')) {
throw new MonitoringException('The "exec" command is required');
}
@ -196,17 +259,49 @@ class RunMonitoring extends Command
return true;
}
final private function checkHttp($task) {
final private function checkDns(Task $task) {
if (! function_exists('exec') || ! is_callable('exec')) {
throw new MonitoringException('The "exec" command is required');
}
if (is_null($task->params) || empty($task->params)) {
throw new Exception('Params are required');
}
$cmd = 'nslookup '.trim($task->params).' '.$task->host;
// If command failed
if (false === $exec = exec($cmd.' '.$task->host, $output, $code)) {
throw new MonitoringException('Unable to execute DNS lookup');
}
// If command returned a non-zero code
if ($code > 0) {
throw new MonitoringException('DNS lookup task failed ('.$exec.')');
}
return true;
}
final private function checkRequest(Task $task, $protocol = CURLPROTO_HTTP | CURLPROTO_HTTPS) {
if (app()->environment() == 'local') {
//throw new MonitoringException('Forcing error for testing');
}
// Preparing cURL
$opts = [
CURLOPT_HEADER => true,
CURLOPT_HTTPGET => true,
CURLOPT_FRESH_CONNECT => true,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_PROTOCOLS => $protocol,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_FAILONERROR => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_URL => trim($task->host)
];
@ -214,16 +309,24 @@ class RunMonitoring extends Command
$ch = curl_init();
curl_setopt_array($ch, $opts);
if ($result = curl_exec($ch)) {
$duration = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
// We have nothing to check into the page
// So for me, this is a big YES
if (empty($task->params)) {
return true;
return [
'result' => true,
'duration' => $duration
];
}
// We are looking for a string in the page
else {
if (strpos($result, $task->params) !== false) {
return true;
return [
'result' => true,
'output' => 'String was found in the page',
'duration' => $duration
];
}
else {
throw new MonitoringException('Cannot find the required string into the page');

View file

@ -0,0 +1,94 @@
<?php
namespace App\Console\Commands;
use \Exception;
use App\Models\Notification;
use App\Mail\TaskNotification;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendNotifications extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitolite:notify
{--limit=1000 : maximum notifications to process at once }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends the notifications alerts';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$notifications = Notification::with(['contact', 'task_history', 'task_history.task'])
->where('status', '=', 'pending')
->orderBy('created_at', 'ASC')
->limit($this->option('limit'), 1000)
->get()
;
$results = [];
if (! empty($notifications)) {
foreach ($notifications as $n) {
if (! isset($results[$n->contact_id])) {
$results[$n->contact_id] = [
'contact' => $n->contact->toArray(),
'tasks' => []
];
}
//else {
$history = $n->task_history;
$task = $history->task;
if (! isset($results[$n->contact_id]['tasks'][$task->id])) {
$results[$n->contact_id]['tasks'][$task->id] = [
'history' => []
];
}
array_push($results[$n->contact_id]['tasks'][$task->id]['history'], $history->toArray());
//}
}
}
if (count($results) > 0) {
foreach ($results as $r) {
$this->info('Sending notifications to '.$r['contact']['email']);
try {
Mail::to($r['contact']['email'])->send(new TaskNotification($r));
Notification::where('contact_id', '=', $r['contact']['id'])->update(
['status' => 'sent']
);
}
catch (Exception $e) {
Notification::where('contact_id', '=', $r['contact']['id'])->update(
['status' => 'error']
);
}
}
}
}
}

View file

@ -1,15 +1,14 @@
<?php
namespace App\Console\Commands;
/**
* R E A D T H I S :
* THIS COMMAND IS FOR MY OWN NEEDS ONLY
* IT SYNCS ALL THE TASKS FROM A DISTANT API
* IT IS PROBABLY WORTHLESS FOR YOU
*/
namespace App\Console\Commands;
use Illuminate\Console\Command;
class SyncCustomers extends Command
@ -19,14 +18,14 @@ class SyncCustomers extends Command
*
* @var string
*/
protected $signature = 'customers:sync';
protected $signature = 'monitolite:sync';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronize all customers';
protected $description = 'Synchronizes all customers\' websites with Monitolite';
/**
* Create a new command instance.
@ -108,14 +107,14 @@ class SyncCustomers extends Command
if (false === array_search(trim($c->domain), $tasks_flat)) {
$ret = app('db')->insert('
INSERT INTO tasks (`host`, `type`, `params`, `creation_date`, `frequency`, `active`, `group_id`)
INSERT INTO tasks (`host`, `type`, `params`, `created_at`, `frequency`, `active`, `group_id`)
VALUES(:host, :type, :params, :creation_date, :frequency, :active, :group_id)
', [
'host' => 'https://'.trim($c->domain),
'type' => 'http',
'params' => 'propulsé par',
'params' => 'restovisio.com',
'creation_date' => date('Y-m-d H:i:s'),
'frequency' => 600,
'frequency' => 3600,
'active' => 1,
'group_id' => $c->id
]);
@ -125,7 +124,7 @@ class SyncCustomers extends Command
// Inserting contacts
foreach ($contacts as $c) {
app('db')->insert('INSERT INTO notifications (`task_id`, `contact_id`) VALUES (:task_id, :contact_id)', [
app('db')->insert('INSERT INTO contact_task (`task_id`, `contact_id`) VALUES (:task_id, :contact_id)', [
'task_id' => $task_id,
'contact_id' => $c->id
]);
@ -147,7 +146,7 @@ class SyncCustomers extends Command
if (false === array_search($t->host, $hosts)) {
// Must delete task
//$this->line('must delete '.$t->host);
app('db')->delete('DELETE FROM `tasks` WHERE host = ?', [$t->host]);
//app('db')->delete('DELETE FROM `tasks` WHERE host = ?', [$t->host]);
}
}
$bar->finish();

View file

@ -5,8 +5,8 @@ namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Laravel\Lumen\Console\Kernel as ConsoleKernel;
use App\Console\Commands\SyncCustomers;
use App\Console\Commands\CleanHistory;
use App\Console\Commands\RunMonitoring;
use App\Console\Commands\SendNotifications;
class Kernel extends ConsoleKernel
{
@ -17,8 +17,8 @@ class Kernel extends ConsoleKernel
*/
protected $commands = [
SyncCustomers::class,
CleanHistory::class,
RunMonitoring::class
RunMonitoring::class,
SendNotifications::class
];
/**
@ -34,12 +34,17 @@ class Kernel extends ConsoleKernel
* You may safely remove this scheduled task
*/
if (env('CMS_ENABLE_SYNC') == true) {
$schedule->command('monitolite:customers:sync')->hourly();
$schedule->command('monitolite:sync')->hourly();
}
/**
* This is the main monitoring task
*/
$schedule->command('monitolite:monitoring:run')->everyMinute();
$schedule->command('monitolite:run')->everyMinute();
/**
* Send all the notifications
*/
$schedule->command('monitolite:notify')->everyMinute();
}
}

View file

@ -3,6 +3,8 @@
namespace App\Http\Controllers;
use Exception;
use \Carbon\Carbon;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -22,14 +24,15 @@ class ApiController extends Controller
public function getTasks() {
$tasks = [];
$query = DB::select('
SELECT DISTINCT t.id, t.host, t.type, t.params, t.frequency, t.creation_date, t.last_execution, t.active, t.group_id, h.status, h.output, g.name as group_name
FROM `tasks` as t
LEFT JOIN `tasks_history` as h ON (h.task_id = t.id)
LEFT JOIN `groups` as g ON (g.id = t.group_id)
WHERE (t.last_execution IS NULL OR h.datetime = t.last_execution)
ORDER BY group_name ASC
');
$query = Task
::leftJoin('groups', 'groups.id', 'tasks.group_id')
->select(
'tasks.id', 'tasks.host', 'tasks.status', 'tasks.type', 'tasks.params', 'tasks.frequency', 'tasks.created_at', 'tasks.executed_at', 'tasks.active', 'tasks.group_id',
'groups.name as group_name')
->get()
;
//dd($query->toSql());
foreach ($query as $t) {
if (is_null($t->group_id)) {
@ -54,44 +57,111 @@ class ApiController extends Controller
return response()->json($tasks);
}
public function getTaskDetails(Request $request, $id) {
$days = ($request->input('days', 15) - 1);
public function getTaskDetails($id) {
$query = DB::select('
SELECT t.id, t.host, t.type, t.params, t.frequency, t.creation_date, t.last_execution, t.active, t.group_id, h.status, h.output, g.name as group_name
FROM `tasks` as t
LEFT JOIN `tasks_history` as h ON (h.task_id = t.id)
LEFT JOIN `groups` as g ON (g.id = t.group_id)
WHERE (t.last_execution IS NULL OR h.datetime = t.last_execution) AND t.id = :task_id
LIMIT 1
', [
'task_id' => $id
]);
$task = Task::with(['group'])
->findOrFail($id)
;
if ($query) {
foreach ($query as $q) {
return response()->json($q);
if (! is_null($task)) {
// First, we get the first date of the stats
// In this case, one month ago
$first_day = Carbon::now()->startOfDay()->subDays($days);
// Then we get all history for the past month
$history = $task
->history()
->orderBy('created_at', 'desc')
->where('created_at', '>', $first_day->toDateString())
->selectRaw('id, date(created_at) as date, created_at, status, duration, output')
->get()
;
// Then we start building an array for the entire month
$stats = $times = [];
$tmpdate = Carbon::now()->subDays($days);
do {
$stats['uptime'][$tmpdate->toDateString()] = [
'up' => 0,
'down' => 0
];
$stats['times'][$tmpdate->toDateString()] = [
'duration' => 0,
'count' => 0
];
$tmpdate = $tmpdate->addDay();
}
while ($tmpdate->lt(Carbon::now()));
// Then we populate the stats data
$prev = null;
if (! is_null($history)) {
$history = $history->reverse();
foreach ($history as $k => $r) {
if (empty($stats['uptime'][$r->date])) {
$stats['uptime'][$r->date] = [
'up' => 0,
'down' => 0
];
}
// Populating the stats
if ($r->status == 1) {
++$stats['uptime'][$r->date]['up'];
}
else {
++$stats['uptime'][$r->date]['down'];
}
// Populating the response times
if ($r->status == 1 && $r->duration > 0) {
$stats['times'][$r->date]['duration'] += $r->duration;
$stats['times'][$r->date]['count'] ++;
}
// We only take tasks when status has changed between them
if (! is_null($prev) && $r->status == $prev) {
unset($history[$k]);
}
$prev = $r->status;
}
}
// Getting the notifications sent
$notifications = $task
->notifications()
->with(['contact', 'task_history'])
->where('notifications.created_at', '>', $first_day->toDateString())
->orderBy('notifications.created_at', 'desc')
->get()
;
return response()->json([
'task' => $task,
'stats' => $stats,
'history' => $history,
'notifications' => $notifications,
'first_day' => $first_day->toDateTimeString()
]);
}
}
public function toggleTaskStatus(Request $request, $id) {
if($active = $request->input('active')) {
//throw new ApiException('Invalid parameters');
}
$active = $request->input('active', null);
if (is_null($active)) {
throw new ApiException('Invalid parameters');
}
$active = intval($active);
$query = DB::update('
UPDATE tasks
SET active = :active
WHERE id = :id
', [
'active' => $active,
'id' => $id
]);
$task = Task::findOrFail($id);
$task->active = $active;
if ($query !== false) {
return $this->getTaskDetails($id);
if ($task->save()) {
return response()->json($task);
}
else {
throw new ApiException('Cannot disable this task');

View file

@ -1,26 +0,0 @@
<?php
namespace App\Jobs;
class ExampleJob extends Job
{
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Mail;
use App\Models\Notification;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class TaskNotification extends Mailable
{
use SerializesModels;
/**
* The order instance.
*
* @var \App\Models\Order
*/
protected $report;
/**
* Create a new message instance.
*
* @param \App\Models\Order $order
* @return void
*/
public function __construct($report)
{
$this->report = $report;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this
->subject('Monitolite Alert Report')
->from(env('MAIL_FROM_ADDRESS', 'noreply@monitolite.fr'), env('MAIL_FROM_NAME', 'Monitolite'))
->markdown('emails.notification')
->with([
'report' => $this->report,
'url' => env('APP_URL')
])
;
}
}

22
app/Models/Contact.php Normal file
View file

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Contact extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
}

24
app/Models/Group.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Group extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
public function tasks() {
return $this->hasMany('App\Models\Task');
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Notification extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
public function contact() {
return $this->belongsTo('App\Models\Contact');
}
public function task_history() {
return $this->belongsTo('App\Models\TaskHistory');
}
public static function addNotificationTask(TaskHistory $history) {
$contacts = $history->task->contacts()->get();
if (! is_null($contacts)) {
foreach ($contacts as $c) {
$notification = new Notification;
$notification->contact_id = $c->id;
$notification->task_history_id = $history->id;
$notification->status = 'pending';
$notification->save();
}
}
}
}

40
app/Models/Task.php Normal file
View file

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
public $timestamps = [
'created_at',
'updated_at',
'executed_at'
];
public function group() {
return $this->belongsTo('App\Models\Group');
}
public function contacts() {
return $this->belongsToMany('App\Models\Contact');
}
public function history() {
return $this->hasMany('App\Models\TaskHistory');
}
public function notifications() {
return $this->hasManyThrough('App\Models\Notification', 'App\Models\TaskHistory');
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TaskContact extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TaskHistory extends Model
{
use HasFactory;
protected $table = 'task_history';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
public function notifications() {
return $this->hasMany('App\Models\Notification');
}
public function task() {
return $this->belongsTo('App\Models\Task');
}
}

View file

@ -25,7 +25,7 @@ $app = new Laravel\Lumen\Application(
$app->withFacades();
// $app->withEloquent();
$app->withEloquent();
/*
|--------------------------------------------------------------------------
@ -59,7 +59,25 @@ $app->singleton(
|
*/
/**
* This is a required HACK for Lumen
*
* @return string
*/
function setDbTimezone() {
$offset = timezone_offset_get(new \DateTimeZone(date_default_timezone_get()), new \DateTime());
return sprintf("%s%02d:%02d", ($offset >= 0) ? '+' : '-', abs($offset / 3600), abs($offset % 3600));
}
$app->configure('app');
$app->configure('database');
$app->configure('mail');
$app->alias('mail.manager', Illuminate\Mail\MailManager::class);
$app->alias('mail.manager', Illuminate\Contracts\Mail\Factory::class);
$app->alias('mailer', Illuminate\Mail\Mailer::class);
$app->alias('mailer', Illuminate\Contracts\Mail\Mailer::class);
$app->alias('mailer', Illuminate\Contracts\Mail\MailQueue::class);
/*
|--------------------------------------------------------------------------
@ -94,6 +112,7 @@ $app->configure('app');
// $app->register(App\Providers\AppServiceProvider::class);
// $app->register(App\Providers\AuthServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);
$app->register(Illuminate\Mail\MailServiceProvider::class);
/*
|--------------------------------------------------------------------------

View file

@ -6,6 +6,7 @@
"type": "project",
"require": {
"php": "^7.3|^8.0",
"illuminate/mail": "^8.77",
"laravel/lumen-framework": "^8.3.1"
},
"require-dev": {

745
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "43a8a6e444590514bb4b9f6a02112d0f",
"content-hash": "5c34208f62c29296617bdbe23b54102e",
"packages": [
{
"name": "brick/math",
@ -66,6 +66,81 @@
],
"time": "2021-08-15T20:50:18+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "0992cc19268b259a39e86f296da5f0677841f42c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/0992cc19268b259a39e86f296da5f0677841f42c",
"reference": "0992cc19268b259a39e86f296da5f0677841f42c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^3.14"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": [
"access",
"data",
"dot",
"notation"
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.1"
},
"time": "2021-08-13T13:06:58+00:00"
},
{
"name": "doctrine/inflector",
"version": "2.0.4",
@ -1352,6 +1427,67 @@
},
"time": "2021-11-16T13:57:03+00:00"
},
{
"name": "illuminate/mail",
"version": "v8.77.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/mail.git",
"reference": "f2f7ea1002d6756bf3a1093e2246b83d41307ff3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/mail/zipball/f2f7ea1002d6756bf3a1093e2246b83d41307ff3",
"reference": "f2f7ea1002d6756bf3a1093e2246b83d41307ff3",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/collections": "^8.0",
"illuminate/container": "^8.0",
"illuminate/contracts": "^8.0",
"illuminate/macroable": "^8.0",
"illuminate/support": "^8.0",
"league/commonmark": "^1.3|^2.0.2",
"php": "^7.3|^8.0",
"psr/log": "^1.0|^2.0",
"swiftmailer/swiftmailer": "^6.3",
"tijsverkoyen/css-to-inline-styles": "^2.2.2"
},
"suggest": {
"aws/aws-sdk-php": "Required to use the SES mail driver (^3.198.1).",
"guzzlehttp/guzzle": "Required to use the Mailgun mail driver (^6.5.5|^7.0.1).",
"wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Mail\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Mail package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2021-12-16T20:33:46+00:00"
},
{
"name": "illuminate/pagination",
"version": "v8.76.2",
@ -2020,6 +2156,191 @@
},
"time": "2021-11-30T15:53:04+00:00"
},
{
"name": "league/commonmark",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "819276bc54e83c160617d3ac0a436c239e479928"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/819276bc54e83c160617d3ac0a436c239e479928",
"reference": "819276bc54e83c160617d3ac0a436c239e479928",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/config": "^1.1.1",
"php": "^7.4 || ^8.0",
"psr/event-dispatcher": "^1.0",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.30.0",
"commonmark/commonmark.js": "0.30.0",
"composer/package-versions-deprecated": "^1.8",
"erusev/parsedown": "^1.0",
"ext-json": "*",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4",
"phpstan/phpstan": "^0.12.88 || ^1.0.0",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.2-dev"
}
},
"autoload": {
"psr-4": {
"League\\CommonMark\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
"homepage": "https://commonmark.thephpleague.com",
"keywords": [
"commonmark",
"flavored",
"gfm",
"github",
"github-flavored",
"markdown",
"md",
"parser"
],
"support": {
"docs": "https://commonmark.thephpleague.com/",
"forum": "https://github.com/thephpleague/commonmark/discussions",
"issues": "https://github.com/thephpleague/commonmark/issues",
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
"source": "https://github.com/thephpleague/commonmark"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
"type": "tidelift"
}
],
"time": "2021-12-05T18:25:20+00:00"
},
{
"name": "league/config",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/config.git",
"reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/config/zipball/a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e",
"reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e",
"shasum": ""
},
"require": {
"dflydev/dot-access-data": "^3.0.1",
"nette/schema": "^1.2",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.90",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.2-dev"
}
},
"autoload": {
"psr-4": {
"League\\Config\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Define configuration arrays with strict schemas and access values with dot notation",
"homepage": "https://config.thephpleague.com",
"keywords": [
"array",
"config",
"configuration",
"dot",
"dot-access",
"nested",
"schema"
],
"support": {
"docs": "https://config.thephpleague.com/",
"issues": "https://github.com/thephpleague/config/issues",
"rss": "https://github.com/thephpleague/config/releases.atom",
"source": "https://github.com/thephpleague/config"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
}
],
"time": "2021-08-14T12:15:32+00:00"
},
{
"name": "monolog/monolog",
"version": "2.3.5",
@ -2215,6 +2536,153 @@
],
"time": "2021-12-03T14:59:52+00:00"
},
{
"name": "nette/schema",
"version": "v1.2.2",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df",
"reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df",
"shasum": ""
},
"require": {
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0",
"php": ">=7.1 <8.2"
},
"require-dev": {
"nette/tester": "^2.3 || ^2.4",
"phpstan/phpstan-nette": "^0.12",
"tracy/tracy": "^2.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "📐 Nette Schema: validating data structures against a given Schema.",
"homepage": "https://nette.org",
"keywords": [
"config",
"nette"
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.2.2"
},
"time": "2021-10-15T11:40:02+00:00"
},
{
"name": "nette/utils",
"version": "v3.2.6",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "2f261e55bd6a12057442045bf2c249806abc1d02"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/2f261e55bd6a12057442045bf2c249806abc1d02",
"reference": "2f261e55bd6a12057442045bf2c249806abc1d02",
"shasum": ""
},
"require": {
"php": ">=7.2 <8.2"
},
"conflict": {
"nette/di": "<3.0.6"
},
"require-dev": {
"nette/tester": "~2.0",
"phpstan/phpstan": "^1.0",
"tracy/tracy": "^2.3"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()",
"ext-xml": "to use Strings::length() etc. when mbstring is not available"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v3.2.6"
},
"time": "2021-11-24T15:47:23+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
@ -2777,6 +3245,82 @@
],
"time": "2021-09-25T23:10:38+00:00"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v6.3.0",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
"reference": "8a5d5072dca8f48460fce2f4131fcc495eec654c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8a5d5072dca8f48460fce2f4131fcc495eec654c",
"reference": "8a5d5072dca8f48460fce2f4131fcc495eec654c",
"shasum": ""
},
"require": {
"egulias/email-validator": "^2.0|^3.1",
"php": ">=7.0.0",
"symfony/polyfill-iconv": "^1.0",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"symfony/phpunit-bridge": "^4.4|^5.4"
},
"suggest": {
"ext-intl": "Needed to support internationalized email addresses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
}
},
"autoload": {
"files": [
"lib/swift_required.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Corbyn"
},
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
}
],
"description": "Swiftmailer, free feature-rich PHP mailer",
"homepage": "https://swiftmailer.symfony.com",
"keywords": [
"email",
"mail",
"mailer"
],
"support": {
"issues": "https://github.com/swiftmailer/swiftmailer/issues",
"source": "https://github.com/swiftmailer/swiftmailer/tree/v6.3.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer",
"type": "tidelift"
}
],
"abandoned": "symfony/mailer",
"time": "2021-10-18T15:26:12+00:00"
},
{
"name": "symfony/console",
"version": "v5.4.1",
@ -2876,6 +3420,72 @@
],
"time": "2021-12-09T11:22:43+00:00"
},
{
"name": "symfony/css-selector",
"version": "v5.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "44b933f98bb4b5220d10bed9ce5662f8c2d13dcc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/44b933f98bb4b5220d10bed9ce5662f8c2d13dcc",
"reference": "44b933f98bb4b5220d10bed9ce5662f8c2d13dcc",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.16"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v5.4.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-09-09T08:06:01+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.5.0",
@ -3588,6 +4198,86 @@
],
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/polyfill-iconv",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
"reference": "63b5bb7db83e5673936d6e3b8b3e022ff6474933"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/63b5bb7db83e5673936d6e3b8b3e022ff6474933",
"reference": "63b5bb7db83e5673936d6e3b8b3e022ff6474933",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-iconv": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Iconv\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Iconv extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"iconv",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-iconv/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T09:27:20+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.23.1",
@ -4732,6 +5422,59 @@
],
"time": "2021-12-01T15:04:08+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.4",
"source": {
"type": "git",
"url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
"reference": "da444caae6aca7a19c0c140f68c6182e337d5b1c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/da444caae6aca7a19c0c140f68c6182e337d5b1c",
"reference": "da444caae6aca7a19c0c140f68c6182e337d5b1c",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"php": "^5.5 || ^7.0 || ^8.0",
"symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2.x-dev"
}
},
"autoload": {
"psr-4": {
"TijsVerkoyen\\CssToInlineStyles\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Tijs Verkoyen",
"email": "css_to_inline_styles@verkoyen.eu",
"role": "Developer"
}
],
"description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.",
"homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
"support": {
"issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
"source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.4"
},
"time": "2021-12-08T09:12:39+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.4.1",

137
config/database.php Normal file
View file

@ -0,0 +1,137 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for all database work. Of course
| you may use many connections at once using the Database library.
|
*/
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => env('DB_PREFIX', ''),
],
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'strict' => env('DB_STRICT_MODE', true),
'engine' => env('DB_ENGINE', null),
'timezone' => setDbTimezone(),
],
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 5432),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => env('DB_PREFIX', ''),
'schema' => env('DB_SCHEMA', 'public'),
'sslmode' => env('DB_SSL_MODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 1433),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => env('DB_PREFIX', ''),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run in the database.
|
*/
'migrations' => 'migrations',
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer set of commands than a typical key-value systems
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'lumen'), '_').'_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
];

117
config/mail.php Normal file
View file

@ -0,0 +1,117 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send any email
| messages sent by your application. Alternative mailers may be setup
| and used as needed; however, this mailer will be used by default.
|
*/
'default' => env('MAIL_MAILER', 'smtp'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers to be used while
| sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required.
|
| Supported: "smtp", "sendmail", "mailgun", "ses",
| "postmark", "log", "array", "failover"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
],
'ses' => [
'transport' => 'ses',
],
'mailgun' => [
'transport' => 'mailgun',
],
'postmark' => [
'transport' => 'postmark',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'noreply@monitolite.fr'),
'name' => env('MAIL_FROM_NAME', 'Monitolite'),
],
/*
|--------------------------------------------------------------------------
| Markdown Mail Settings
|--------------------------------------------------------------------------
|
| If you are using Markdown based email rendering, you may configure your
| theme and component paths here, allowing you to customize the design
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
];

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateContactTaskTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('contact_task', function (Blueprint $table) {
$table->bigInteger('task_id')->unsigned();
$table->bigInteger('contact_id')->unsigned();
$table->primary(['task_id', 'contact_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('contact_task');
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateContactsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('surname', 200);
$table->string('firstname', 200);
$table->string('email', 250);
$table->string('phone', 20);
$table->timestamps();
$table->tinyInteger('active')->default(1);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('contacts');
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateGroupsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('groups', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name', 128)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('groups');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateNotificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('notifications', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('contact_id');
$table->unsignedBigInteger('task_history_id');
$table->enum('status', ['pending', 'sent', 'error'])->default('pending');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('notifications');
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTaskHistoryTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('task_history', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedTinyInteger('status');
$table->text('output')->nullable();
$table->float('duration', 5, 3)->unsigned()->nullable();
$table->unsignedBigInteger('task_id');
$table->timestamps();
$table->index(['status', 'created_at']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('task_history');
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTasksArchivesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tasks_archives', function (Blueprint $table) {
$table->bigIncrements('id');
$table->date('day');
$table->unsignedInteger('uptime')->default('0');
$table->unsignedBigInteger('task_id')->default('0');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('tasks_archives');
}
}

View file

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTasksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('host');
$table->enum('type', ['ping', 'http', 'dns', 'ftp']);
$table->string('params')->nullable();
$table->unsignedInteger('frequency');
$table->unsignedTinyInteger('attempts')->default('0');
$table->unsignedTinyInteger('active')->default(1);
$table->unsignedTinyInteger('status')->nullable();
$table->unsignedBigInteger('group_id')->nullable();
$table->timestamps();
$table->timestamp('executed_at')->nullable();
$table->unique(['host', 'type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('tasks');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeysToContactTaskTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('contact_task', function (Blueprint $table) {
$table->foreign(['task_id'], 'contact_task_ibfk_1')->references(['id'])->on('tasks')->onUpdate('NO ACTION')->onDelete('CASCADE');
$table->foreign(['contact_id'], 'contact_task_ibfk_2')->references(['id'])->on('contacts')->onUpdate('NO ACTION')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('contact_task', function (Blueprint $table) {
$table->dropForeign('contact_task_ibfk_1');
$table->dropForeign('contact_task_ibfk_2');
});
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeysToNotificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('notifications', function (Blueprint $table) {
$table->foreign(['contact_id'], 'contact_id_frgn')->references(['id'])->on('contacts')->onUpdate('NO ACTION')->onDelete('CASCADE');
$table->foreign(['task_history_id'], 'task_history_id_frgn')->references(['id'])->on('task_history')->onUpdate('NO ACTION')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('notifications', function (Blueprint $table) {
$table->dropForeign('contact_id_frgn');
$table->dropForeign('task_history_id_frgn');
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeysToTaskHistoryTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('task_history', function (Blueprint $table) {
$table->foreign(['task_id'], 'task_history_ibfk_1')->references(['id'])->on('tasks')->onUpdate('NO ACTION')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('task_history', function (Blueprint $table) {
$table->dropForeign('task_history_ibfk_1');
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeysToTasksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->foreign(['group_id'], 'group_id_frgn')->references(['id'])->on('groups')->onUpdate('NO ACTION')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropForeign('group_id_frgn');
});
}
}

View file

@ -1,350 +0,0 @@
#!/usr/bin/perl
################################
# #
# M O N I T O L I T E #
# #
# Lightweight Monitoring Tool #
# #
# @author: Axel de Vignon #
# @copyright: www.vidax.net #
# @license: Mozilla Public 1.1 #
# #
################################
use warnings;
use strict;
use DBI;
use Dotenv;
use Net::Ping;
use Email::MIME;
use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::SMTP qw();
use LWP::Simple;
use LWP::UserAgent;
use LWP::Protocol::https;
my $query;
my $result;
my $tasks;
my $update_query;
my $emails;
my $email;
my $message;
my $response;
my $html;
my $numtasks;
my $previous_status;
my $subject;
my $datas;
############################
# #
# S E T T I N G S #
# #
############################
Dotenv->load;
my $dbtype = $ENV{'DB_CONNECTION'};
my $hostname = $ENV{'DB_HOST'};
my $database = $ENV{'DB_DATABASE'};
my $login = $ENV{'DB_USERNAME'};
my $port = $ENV{'DB_PORT'};
my $password = $ENV{'DB_PASSWORD'};
my $email_from = $ENV{'MAIL_FROM'};
my $number_tries = $ENV{'NB_TRIES'};
my $days_history_archive = $ENV{'ARCHIVE_DAYS'};
my $smtp_host = $ENV{'SMTP_HOST'};
my $smtp_user = $ENV{'SMTP_USER'};
my $smtp_password = $ENV{'SMTP_PASSWORD'};
my $smtp_port = $ENV{'SMTP_PORT'};
my $smtp_ssl = $ENV{'SMTP_SSL'};
############################
######
# Testing database connection
######
my $dsn = "DBI:$dbtype:database=$database;host=$hostname;port=$port";
my $dbh = DBI->connect($dsn, $login, $password) or output('cannot connect to database', 'ERROR', 1);
######
# Getting tasks
######
my $execution_time = server_time();
my $query1 = $dbh->prepare('SELECT id, host, type, params FROM tasks WHERE ( DATE_SUB(now(), INTERVAL frequency SECOND) > last_execution OR last_execution IS NULL ) AND active = 1');
$query1->execute() or output('Cannot execute query fetching all pending tasks', 'ERROR', 1);
$numtasks = $query1->rows;
#####
# Processing all tasks
#####
if ($numtasks > 0) {
while ($tasks = $query1->fetchrow_hashref()) {
print "\n";
my $status = -1;
$previous_status = -1;
$message = 'Host is back up';
####
# Getting last history for this host
####
my $query2 = $dbh->prepare('SELECT status FROM tasks_history WHERE task_id = ' . $tasks->{'id'} . ' ORDER BY datetime DESC LIMIT 1');
$query2->execute() or output('Cannot get history for this task', 'ERROR', 0);
if ($query2->rows > 0) {
my $history = $query2->fetchrow_hashref();
$previous_status = $history->{'status'};
}
if ($tasks->{'type'} =~ 'ping') {
# Ping check returned an error
if (! check_ping($tasks->{'host'})) {
$status = 0;
output('Host "'. $tasks->{'host'} .'" [' . $tasks->{'type'} . '] is down', 'ALERT');
$message = 'Host does not reply to ping. Timed out after 5s. Giving up...';
}
# Ping check went fine
else {
$status = 1;
output('Host "'. $tasks->{'host'} .'" [' . $tasks->{'type'} . '] is up', 'SUCCESS');
}
}
elsif ($tasks->{'type'} =~ 'http') {
$response = check_http($tasks->{'host'}, $tasks->{'params'});
# HTTP check went fine
if ($response =~ 'OK') {
$status = 1;
output('Host "'. $tasks->{'host'} .'" [' . $tasks->{'type'} . '] is up', 'SUCCESS');
}
# HTTP check returned an error
else {
$status = 0;
output('Host "'. $tasks->{'host'} .'" [' . $tasks->{'type'} . '] is down', 'ALERT');
$message = 'HTTP response was: ' . $response;
}
}
else {
output('dunno how to process this task', 'DEBUG');
next;
}
# Notify on status changes only
if ($previous_status != -1 && $status != $previous_status) {
output('Should send notification', 'DEBUG');
&send_notifications($tasks->{'id'}, $tasks->{'host'}, $tasks->{'type'}, $message, $status);
}
# Saving Status into DB
if ($status >= 0) {
save_history($tasks->{'id'}, $status, $execution_time, $response);
}
}
}
else {
output('nothing to monitor, sleeping back', 'DEBUG');
}
#####
# Function used for the PING test
#####
sub check_ping {
my ($host, $round) = @_;
$round = 1 if (! $round);
my $ping = Net::Ping->new('icmp');
output('ping check n°' . $round . ' on ' . $host, 'DEBUG');
if (! $ping->ping($host)) {
$ping->close();
if ($number_tries && $round <= $number_tries) {
sleep (2);
return check_ping($host, $round + 1)
}
else {
return undef;
}
} else {
$ping->close();
return 'OK';
}
}
#####
# Function used to check HTTP service
#####
sub check_http {
my ($host, $find, $round) = @_;
$round = 1 if (! $round);
$host = 'http://'.$host if ($host !~ m/^http/i);
my $check = LWP::UserAgent->new(
ssl_opts => { verify_hostname => 1 },
protocols_allowed => ['http', 'https']
);
$check->timeout(20);
$check->env_proxy;
my $response = $check->get($host, ':content_cb' => \&process_data);
output('http check n°' . $round . ' on ' . $host, 'DEBUG');
if ($response->is_success) {
if ($find && length($find) > 0) {
output('searching "' . $find . '" into html content on ' . $host, 'DEBUG');
if ($html =~ m/$find/i) {
output('html content found, looks fine', 'SUCCESS');
return 'OK';
}
else {
output('html content not found', 'ERROR');
return 'Could not find "' . $find . '" into the page';
}
}
else {
return 'OK';
}
}
else {
output('HTTP response error was: '.$response->status_line, 'DEBUG');
if ($number_tries && $round < $number_tries) {
sleep (2);
return check_http($host, $find, $round + 1);
}
else {
return $response->status_line;
}
}
}
#####
# Save the page HTML content
#####
sub process_data {
my ($content, $handler1, $handler2) = @_;
$html .= $content;
}
#####
# Function managing DEBUG and OUTPUT
#####
sub output {
my ($output, $level, $fatal) = @_;
$output = server_time().' - '.$level.' - '.$output."\n";
if ($fatal && $fatal == 1) {
die ('FATAL '.$output);
}
else {
print ($output);
}
return 1;
}
#####
# Function that keeps an history
#####
sub save_history {
my ($task_id, $status, $datetime, $response) = @_;
my $query = $dbh->prepare('INSERT INTO `tasks_history` (`status`, `datetime`, `task_id`, `output`) VALUES(' . $status . ', "'.$datetime.'", ' . $task_id . ', "' . $response . '")');
if ($query->execute()) {
output('saving status to history', 'DEBUG');
}
else {
output('cannot save status to history', 'ERROR');
}
$update_query = $dbh->prepare('UPDATE `tasks` SET `last_execution` = "'.$datetime.'" WHERE id = ' . $task_id);
if ($update_query->execute()) {
output('saving last execution time for this task', 'DEBUG');
}
else {
output('cannot save last execution time for this task', 'ERROR');
}
return 1;
}
#####
# Function sending notifications
#####
sub send_notifications {
my ($task_id, $host, $type, $message, $status) = @_;
if ($status == 0) {
$subject = 'DOWN: host "' . $host . '" [' . $type . '] is down';
$datas = "------ ALERT DETECTED BY MONITORING SERVICE ------ \n\n\nDATETIME: " . server_time() . " (server time)\nHOST: " . $host . "\nSERVICE: " . $type . "\nMESSAGE: " . $message;
}
else {
$subject = 'UP: host "' . $host . '" [' . $type . '] is up';
$datas = "------ RECOVERY DETECTED BY MONITORING SERVICE ------ \n\n\nDATETIME: " . server_time() . " (server time)\nHOST: " . $host . "\nSERVICE: " . $type . "\nMESSAGE: " . $message;
}
my $query = $dbh->prepare('SELECT c.email FROM contacts as c JOIN notifications as n ON (n.contact_id = c.id) WHERE c.active = 1 AND n.task_id = '.$task_id);
if ($query->execute()) {
while ($emails = $query->fetchrow_hashref()) {
my $email = Email::MIME->create(
header_str => [
From => $email_from,
To => $emails->{'email'},
Subject => $subject
],
parts => [
$datas
],
);
eval {
sendmail(
$email,
{
from => $email_from,
transport => Email::Sender::Transport::SMTP->new({
host => $smtp_host,
port => $smtp_port,
sasl_username => $smtp_user,
sasl_password => $smtp_password,
ssl => $smtp_ssl,
timeout => 10
})
}
);
output('Notification email was sent to '.$emails->{'email'}, 'DEBUG');
};
warn $@ if $@;
}
return 1
}
output('failed to send notifications', 'ERROR');
return undef;
}
#####
# Function getting datetime
#####
sub server_time {
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
my $now = (1900 + $year).'-'.($mon + 1).'-'.$mday.' '.$hour.':'.$min.':00';
return $now;
}

210
package-lock.json generated
View file

@ -1,13 +1,16 @@
{
"name": "web",
"name": "monitolite",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"apexcharts": "^3.32.0",
"axios": "^0.24.0",
"moment": "^2.29.1",
"vue": "^2.6.14",
"vue-apexcharts": "^1.6.2",
"vue-loading-overlay": "^3.4.2",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
},
@ -2396,6 +2399,19 @@
"node": ">= 8"
}
},
"node_modules/apexcharts": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.32.0.tgz",
"integrity": "sha512-9VUyiTR2RgD4NIJOOdKxxi8tjzrKCBMr7HjWxsw+5lDPu/tZJLVudpgFhlNTIy3CbhxQ4edaiTttmbo6eDDgiA==",
"dependencies": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"node_modules/array-flatten": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
@ -8539,6 +8555,89 @@
"node": ">=8"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dependencies": {
"svg.js": "^2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=",
"dependencies": {
"svg.js": ">=2.3.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"node_modules/svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"dependencies": {
"svg.js": "^2.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dependencies": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js/node_modules/svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dependencies": {
"svg.js": "^2.6.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svgo": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
@ -8906,6 +9005,14 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
},
"node_modules/vue-apexcharts": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/vue-apexcharts/-/vue-apexcharts-1.6.2.tgz",
"integrity": "sha512-9HS3scJwWgKjmkcWIf+ndNDR0WytUJD8Ju0V2ZYcjYtlTLwJAf2SKUlBZaQTkDmwje/zMgulvZRi+MXmi+WkKw==",
"peerDependencies": {
"apexcharts": "^3.26.0"
}
},
"node_modules/vue-hot-reload-api": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
@ -8937,6 +9044,18 @@
}
}
},
"node_modules/vue-loading-overlay": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/vue-loading-overlay/-/vue-loading-overlay-3.4.2.tgz",
"integrity": "sha512-xcB+NPjl76eA0uggm707x3ZFgrNosZXpynHipyS3K+rrK1NztOV49R1LY+/4ij5W1KYANp7eRI2EIHrxCpmWAw==",
"engines": {
"node": ">=6.9.0",
"npm": ">=3.10.0"
},
"peerDependencies": {
"vue": "^2.0.0"
}
},
"node_modules/vue-router": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.3.tgz",
@ -11290,6 +11409,19 @@
"picomatch": "^2.0.4"
}
},
"apexcharts": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.32.0.tgz",
"integrity": "sha512-9VUyiTR2RgD4NIJOOdKxxi8tjzrKCBMr7HjWxsw+5lDPu/tZJLVudpgFhlNTIy3CbhxQ4edaiTttmbo6eDDgiA==",
"requires": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"array-flatten": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
@ -15953,6 +16085,70 @@
"has-flag": "^4.0.0"
}
},
"svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"requires": {
"svg.js": "^2.0.1"
}
},
"svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=",
"requires": {
"svg.js": ">=2.3.x"
}
},
"svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=",
"requires": {
"svg.js": "^2.2.5"
}
},
"svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"requires": {
"svg.js": "^2.4.0"
}
},
"svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"requires": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"dependencies": {
"svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"requires": {
"svg.js": "^2.2.5"
}
}
}
},
"svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"requires": {
"svg.js": "^2.6.5"
}
},
"svgo": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
@ -16228,6 +16424,12 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
},
"vue-apexcharts": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/vue-apexcharts/-/vue-apexcharts-1.6.2.tgz",
"integrity": "sha512-9HS3scJwWgKjmkcWIf+ndNDR0WytUJD8Ju0V2ZYcjYtlTLwJAf2SKUlBZaQTkDmwje/zMgulvZRi+MXmi+WkKw==",
"requires": {}
},
"vue-hot-reload-api": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
@ -16247,6 +16449,12 @@
"vue-style-loader": "^4.1.0"
}
},
"vue-loading-overlay": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/vue-loading-overlay/-/vue-loading-overlay-3.4.2.tgz",
"integrity": "sha512-xcB+NPjl76eA0uggm707x3ZFgrNosZXpynHipyS3K+rrK1NztOV49R1LY+/4ij5W1KYANp7eRI2EIHrxCpmWAw==",
"requires": {}
},
"vue-router": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.3.tgz",

View file

@ -8,9 +8,12 @@
"vue-template-compiler": "^2.6.14"
},
"dependencies": {
"apexcharts": "^3.32.0",
"axios": "^0.24.0",
"moment": "^2.29.1",
"vue": "^2.6.14",
"vue-apexcharts": "^1.6.2",
"vue-loading-overlay": "^3.4.2",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
}

View file

@ -1,218 +1,4 @@
@import url(https://fonts.googleapis.com/css2?family=Hind:wght@300;400;500;600;700&display=swap);
@font-face {
font-family: "Digital7";
src: url("/fonts/digital.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
/**
* SETTINGS
**/
* {
padding: 0;
margin: 0;
}
@font-face{font-family:Digital7;font-style:normal;font-weight:400;src:url(/fonts/digital.ttf) format("truetype")}*{margin:0;padding:0}html{font-size:100%;scroll-behavior:smooth}html body{background-attachment:fixed;background-image:url(../img/bush.png);color:#3d3d3d;font-family:Hind,sans-serif;font-size:1rem;padding:10px}html body a,html body a:visited{color:inherit;text-decoration:inherit}html body a:hover,html body a:visited:hover{text-decoration:underline}html body .container{margin:0 auto;max-width:1000px;padding:0}html body h1,html body h2,html body h3,html body h4{margin-bottom:.8rem;margin-top:.8rem}html body h1{font-size:2.4rem;margin:3rem 0;text-align:center}html body h2{font-size:1.4rem;margin-top:0;padding-top:0}html body h3{background-color:#0a9f9a;color:#f0f0f0;font-size:1.4rem;margin:0;padding:.8rem;position:relative}html body h3 small{font-size:.9rem}html body h3 .context-menu{cursor:pointer;font-size:1rem;position:absolute;right:.7rem;top:.7rem}html body div.round{background-color:#fff;border-radius:5px;box-shadow:3px 3px 6px 0 rgba(0,0,0,.3);margin-bottom:3rem;overflow:hidden;position:relative}html body div.round h3{margin-bottom:1rem}html body img{vertical-align:sub}html body table{border:1px solid #abc;border-collapse:collapse;border-spacing:0;font-size:14px}html body table th{background-color:#e6eeee}html body table td,html body table th{border:1px solid #9ccece;padding:.3rem}html body table td{background-color:#fff;color:#3d3d3d;text-align:center}html body table td,html body table td img{vertical-align:middle}html body table td.right{text-align:right}html body table#contacts_tbl,html body table#tasks_tbl{width:100%}html body .no-data{color:#727272;font-size:.9rem;font-style:italic;margin-bottom:1.3rem;text-align:center}html body .quick-view .new-group{border-radius:.4rem;cursor:pointer;display:inline-block;margin:.2rem;overflow:hidden}html body .quick-view .new-group .square{float:left;height:100%;line-height:1.2rem;margin:0;min-width:1.4rem;padding:.2rem .6rem;text-align:center;vertical-align:middle}html body .quick-view .new-group .square:not(:first-of-type){border-left:1px solid #fff}html body .tasks .task{background-color:#fff;border-radius:5px;box-shadow:3px 3px 6px 0 rgba(0,0,0,.3);margin-top:2rem;overflow:hidden;padding:0;position:relative}html body .spacer{clear:both;line-height:0;margin:0;padding:0}html body .block-content{padding:.8rem}html body .highlight{background-color:#166260;border-radius:.5rem;color:#fff;display:inline-block;font-size:1rem;padding:0 1rem;vertical-align:middle}html body .small{font-size:.8rem}html body .hidden{display:none}html body .up{background-color:#8adf8a}html body .down{background-color:#f79292}html body .unknown{background-color:#f5d69e}html body .inactive{background-color:#dfdfdf!important;opacity:.5}html body .refreshed-time{font-size:.8rem;margin-bottom:2rem;text-align:right}html body .refreshed-time .clock{background-color:#000;border-radius:4px;color:#fff;font-family:Digital7;font-size:1.2rem;padding:.3rem .5rem}@-webkit-keyframes shake{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}
html {
font-size: 100%;
scroll-behavior: smooth;
}
html body {
padding: 10px;
font-family: "Hind", sans-serif;
font-size: 1rem;
color: #3D3D3D;
background-image: url(../img/bush.png);
background-attachment: fixed;
}
html body a, html body a:visited {
color: inherit;
text-decoration: inherit;
}
html body a:hover, html body a:visited:hover {
text-decoration: underline;
}
html body .container {
padding: 0;
margin: 0 auto;
max-width: 1000px;
}
html body h1, html body h2, html body h3, html body h4 {
margin-top: 0.8rem;
margin-bottom: 0.8rem;
}
html body h1 {
font-size: 2.4rem;
text-align: center;
margin: 3rem 0;
}
html body h2 {
font-size: 1.4rem;
margin-top: 0;
padding-top: 0;
}
html body h3 {
font-size: 1.4rem;
background-color: #0a9f9a;
margin: 0;
padding: 0.8rem;
color: #f0f0f0;
position: relative;
}
html body h3 small {
font-size: 0.9rem;
}
html body h3 .context-menu {
position: absolute;
right: 0.7rem;
top: 0.7rem;
cursor: pointer;
font-size: 1rem;
}
html body img {
vertical-align: sub;
}
html body table {
border: 1px solid #ABC;
font-size: 14px;
border-spacing: 0;
border-collapse: collapse;
}
html body table th {
background-color: #e6EEEE;
border: 1px solid #9ccece;
padding: 0.3rem;
}
html body table td {
color: #3D3D3D;
padding: 0.3rem;
background-color: #FFF;
border: 1px solid #9ccece;
text-align: center;
vertical-align: middle;
}
html body table td img {
vertical-align: middle;
}
html body table td.right {
text-align: right;
}
html body table#tasks_tbl, html body table#contacts_tbl {
width: 100%;
}
html body .quick-view {
box-shadow: 3px 3px 6px 0px rgba(0, 0, 0, 0.3);
border-radius: 5px;
position: relative;
background-color: white;
overflow: hidden;
}
html body .quick-view .new-group {
margin: 0.2rem;
display: inline-block;
border-radius: 0.4rem;
overflow: hidden;
cursor: pointer;
}
html body .quick-view .new-group .square {
height: 100%;
margin: 0;
text-align: center;
vertical-align: middle;
float: left;
line-height: 1.2rem;
min-width: 1.4rem;
padding: 0.2rem 0.6rem;
}
html body .quick-view .new-group .square:not(:first-of-type) {
border-left: 1px solid white;
}
html body .tasks .task {
margin-top: 2rem;
padding: 0;
box-shadow: 3px 3px 6px 0px rgba(0, 0, 0, 0.3);
border-radius: 5px;
position: relative;
background-color: white;
overflow: hidden;
}
html body .spacer {
clear: both;
line-height: 0;
padding: 0;
margin: 0;
}
html body .block-content {
padding: 0.8rem;
}
html body .highlight {
background-color: #166260;
padding: 0px 1rem;
display: inline-block;
color: #FFF;
vertical-align: middle;
border-radius: 0.5rem;
font-size: 1rem;
}
html body .small {
font-size: 0.8rem;
}
html body .hidden {
display: none;
}
html body .up {
background-color: #8adf8a;
}
html body .down {
background-color: #f79292;
}
html body .unknown {
background-color: #f5d69e;
}
html body .inactive {
background-color: #dfdfdf !important;
opacity: 0.5;
}
html body .refreshed-time {
text-align: right;
font-size: 0.8rem;
margin-bottom: 2rem;
}
html body .refreshed-time .clock {
font-family: Digital7;
font-size: 1.2rem;
background-color: #000;
border-radius: 4px;
color: #FFF;
padding: 0.3rem 0.5rem;
}
@-webkit-keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}
/*# sourceMappingURL=app.css.map*/

1
public/css/app.css.map Normal file

File diff suppressed because one or more lines are too long

1
public/img/dns.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 272 272" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#424242;}.cls-2{fill:#f3f4f2;}</style></defs><title/><rect class="cls-1" height="8" width="136" x="68" y="68"/><rect class="cls-1" height="8" width="136" x="68" y="92"/><rect class="cls-1" height="8" width="136" x="68" y="116"/><rect class="cls-1" height="8" width="136" x="68" y="140"/><rect class="cls-1" height="8" width="136" x="68" y="164"/><rect class="cls-1" height="8" width="136" x="68" y="188"/><rect class="cls-1" height="8" width="136" x="68" y="212"/><path class="cls-1" d="M216,40H192V16l-8-8H48V264H224V48Zm0,216H56V16H184V48h32Z"/><rect class="cls-2" height="8" width="136" x="68" y="68"/><rect class="cls-2" height="8" width="136" x="68" y="92"/><rect class="cls-2" height="8" width="136" x="68" y="116"/><rect class="cls-2" height="8" width="136" x="68" y="140"/><rect class="cls-2" height="8" width="136" x="68" y="164"/><rect class="cls-2" height="8" width="136" x="68" y="188"/><rect class="cls-2" height="8" width="136" x="68" y="212"/><path class="cls-1" d="M184,8V48h40Zm8,32V27.31L204.69,40Z"/><rect class="cls-1" height="8" width="136" x="68" y="68"/><rect class="cls-1" height="8" width="136" x="68" y="92"/><rect class="cls-1" height="8" width="136" x="68" y="116"/><rect class="cls-1" height="8" width="136" x="68" y="140"/><rect class="cls-1" height="8" width="136" x="68" y="164"/><rect class="cls-1" height="8" width="136" x="68" y="188"/><rect class="cls-1" height="8" width="136" x="68" y="212"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
public/img/ftp.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M7,25A7,7,0,0,1,7,11a1,1,0,0,1,0,2A5,5,0,0,0,7,23a1,1,0,0,1,0,2Z"/><path d="M25,25a1,1,0,0,1,0-2,5,5,0,0,0,0-10,1,1,0,0,1,0-2,7,7,0,0,1,0,14Z"/><path d="M25,13a1,1,0,0,1-1-1A8,8,0,0,0,8,12a1,1,0,0,1-2,0,10,10,0,0,1,20,0A1,1,0,0,1,25,13Z"/><path d="M21,22H11a1,1,0,0,1,0-2H21a1,1,0,0,1,0,2Z"/><path d="M21,28H11a1,1,0,0,1,0-2H21a1,1,0,0,1,0,2Z"/><path d="M21,28a1,1,0,0,1-.83-.45l-2-3a1,1,0,1,1,1.66-1.1l2,3a1,1,0,0,1-.28,1.38A.94.94,0,0,1,21,28Z"/><path d="M19,31a.94.94,0,0,1-.55-.17,1,1,0,0,1-.28-1.38l2-3a1,1,0,0,1,1.66,1.1l-2,3A1,1,0,0,1,19,31Z"/><path d="M13,25a1,1,0,0,1-.83-.45l-2-3a1,1,0,0,1,1.66-1.1l2,3a1,1,0,0,1-.28,1.38A.94.94,0,0,1,13,25Z"/><path d="M11,22a.94.94,0,0,1-.55-.17,1,1,0,0,1-.28-1.38l2-3a1,1,0,0,1,1.66,1.1l-2,3A1,1,0,0,1,11,22Z"/><path d="M9,25H7a1,1,0,0,1,0-2H9a1,1,0,0,1,0,2Z"/><path d="M25,25H23a1,1,0,0,1,0-2h2a1,1,0,0,1,0,2Z"/></svg>

After

Width:  |  Height:  |  Size: 989 B

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,29 @@
/*!
* ApexCharts v3.32.0
* (c) 2018-2021 ApexCharts
* Released under the MIT License.
*/
/*!
* Vue.js v2.6.14
* (c) 2014-2021 Evan You
* Released under the MIT License.
*/
/*!
* vuex v3.6.2
* (c) 2021 Evan You
* @license MIT
*/
/*! svg.draggable.js - v2.2.2 - 2019-01-08
* https://github.com/svgdotjs/svg.draggable.js
* Copyright (c) 2019 Wout Fierens; Licensed MIT */
/*! svg.filter.js - v2.0.2 - 2016-02-24
* https://github.com/wout/svg.filter.js
* Copyright (c) 2016 Wout Fierens; Licensed MIT */
//! moment.js
//! moment.js locale configuration

1
public/js/app.js.map Normal file

File diff suppressed because one or more lines are too long

View file

@ -14,6 +14,23 @@ Vue.prototype.$http = axios
import moment from 'moment'
Vue.prototype.moment = moment
import VueApexCharts from 'vue-apexcharts'
Vue.use(VueApexCharts)
Vue.component('apexchart', VueApexCharts)
import VueLoading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/vue-loading.css';
Vue.use(VueLoading, {
// Optional parameters
//container: this.fullPage ? null : this.$refs.formContainer,
canCancel: true,
backgroundColor: '#000',
color: '#0a9f9a',
width: 128,
height: 128,
opacity: 0.9,
loader: 'dots'
})
import Home from '../views/app.vue'
import TaskDetails from '../views/taskdetails.vue'

View file

@ -90,49 +90,7 @@ html {
}
}
img {
vertical-align: sub;
}
table {
border: 1px solid #ABC;
font-size: 14px;
border-spacing : 0;
border-collapse : collapse;
th {
background-color: #e6EEEE;
border: 1px solid #9ccece;
padding: 0.3rem;
}
td {
color: #3D3D3D;
padding: 0.3rem;
background-color: #FFF;
border: 1px solid #9ccece;
text-align: center;
vertical-align: middle;
img {
vertical-align: middle;
}
&.right {
text-align: right;
}
}
&#tasks_tbl, &#contacts_tbl {
width: 100%;
}
}
.quick-view {
div.round {
-webkit-box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
-moz-box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
@ -140,6 +98,63 @@ html {
position: relative;
background-color: white;
overflow: hidden;
margin-bottom: 3rem;
h3 {
margin-bottom: 1rem;
}
}
img {
vertical-align: sub;
}
table {
border: 1px solid #ABC;
font-size: 14px;
border-spacing : 0;
border-collapse : collapse;
th {
background-color: #e6EEEE;
border: 1px solid #9ccece;
padding: 0.3rem;
}
td {
color: #3D3D3D;
padding: 0.3rem;
background-color: #FFF;
border: 1px solid #9ccece;
text-align: center;
vertical-align: middle;
img {
vertical-align: middle;
}
&.right {
text-align: right;
}
}
&#tasks_tbl, &#contacts_tbl {
width: 100%;
}
}
.no-data {
text-align: center;
font-style: italic;
margin-bottom: 1.3rem;
font-size: .9rem;
color: #727272;
}
.quick-view {
.new-group {
margin: .2rem;

View file

@ -4,8 +4,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>MonitoLite - Network monitoring tool</title>
<script type="text/javascript" src="/js/app.js"></script>
<link type="text/css" rel="stylesheet" href="/css/app.css" />
<script type="text/javascript" src="{{ url('js/app.js') }}"></script>
<link type="text/css" rel="stylesheet" href="{{ url('css/app.css') }}" />
</head>
<body>

View file

@ -11,15 +11,18 @@
import TaskList from './components/tasklist.vue'
import QuickView from './components/quickview.vue'
export default{
components: {
QuickView,
TaskList
TaskList,
},
data: function() {
return {
refreshed_time: null
refreshed_time: null,
refresh: null,
loading: true,
color: '#FF0000',
size: '10rem',
}
},
computed: {
@ -31,14 +34,23 @@
getTasks: function() {
this.$http.get('/api/getTasks')
.then(response => this.$store.commit('setTasks', response.data))
.catch(error => {
this.loading.hide()
clearTimeout(this.refresh)
window.alert('An error occurred when getting tasks. Automatic refresh has been disabled. You should fix and reload this page.')
})
.then(() => {
this.refreshed_time = this.moment();
this.loading.hide()
})
.catch(error => window.alert('Cannot get tasks'))
this.refreshed_time = this.moment();
}
},
beforeRouteLeave(to, from, next) {
clearTimeout(this.refresh)
next();
},
mounted: function() {
this.loading = this.$loading.show()
this.getTasks()
this.refresh = window.setInterval(() => {
this.getTasks();

View file

@ -1,28 +1,39 @@
<template>
<div class="quick-view">
<div class="quick-view round">
<h3>
Quick overview
</h3>
<div class="block-content">
<div
v-for="group in tasks"
v-bind:key="group.id"
class="new-group"
:title="'Group: '+group.name"
v-if="tasks && Object.keys(tasks).length > 0"
>
<a :href="'#group-'+group.id">
<p
v-for="task in group.tasks"
v-bind:key="task.id"
:href="'#task-'+task.id"
:class="statusText(task.status)+(task.active == 0 ? ' inactive' : '')"
class="square"
>
<span class="small">{{task.id }}</span>
</p>
</a>
<div
v-for="group in tasks"
v-bind:key="group.id"
class="new-group"
:title="'Group: '+group.name"
>
<a :href="'#group-'+group.id">
<p
v-for="task in group.tasks"
v-bind:key="task.id"
:href="'#task-'+task.id"
:class="statusText(task.status)+(task.active == 0 ? ' inactive' : '')"
class="square"
>
<span class="small">{{task.id }}</span>
</p>
</a>
</div>
<p class="spacer">&nbsp;</p>
</div>
<div
class="no-data"
v-else
>
Sorry, there is no task here.
</div>
<p class="spacer">&nbsp;</p>
</div>
</div>
</template>

View file

@ -3,7 +3,7 @@
<div
v-for="group in tasks"
v-bind:key="group.id"
class="task"
class="task round"
>
<a :name="'group-'+group.id"></a>
<h3>
@ -17,7 +17,7 @@
<tr>
<th width="5%">Up?</th>
<th width="*">Host</th>
<th width="5%">Type</th>
<th width="10%">Type</th>
<th width="20%">Last checked</th>
<th width="13%">Frequency (min)</th>
<th width="5%">Active</th>
@ -38,14 +38,14 @@
<a :href="task.host" target="_blank">{{ task.host }}</a>
</td>
<td>
<img :src="task.type == 'http' ? '/img/http.svg' : '/img/ping.svg'" width="16" alt="Type of check" :title="'Type: '+task.type" />
<img :src="'/img/'+task.type+'.svg'" width="16" alt="Type of check" :title="'Type: '+task.type" />
{{ task.type.toUpperCase() }}
</td>
<td>
<span
v-if="task.last_execution"
v-if="task.executed_at"
>
{{ moment(task.last_execution).fromNow() }}
<img src="/img/info.svg" alt="Infos" width="16" :title="'Result: '+task.output" />
{{ moment(task.executed_at).fromNow() }}
</span>
<span
v-else
@ -83,7 +83,7 @@ export default {
computed: {
tasks: function() {
return this.$store.state.tasks
}
},
},
methods: {
statusText: function (status) {
@ -99,10 +99,16 @@ export default {
}
},
disableTask: function(task_id, current_status) {
this.loading = this.$loading.show()
this.$http.patch('/api/toggleTaskStatus/'+task_id, {
active: + !current_status
})
.then(response => this.$store.commit('updateTask', response.data))
.then(response => {
this.$store.commit('updateTask', response.data)
})
.then(() => {
this.loading.hide()
})
}
}
}

View file

@ -0,0 +1,24 @@
@component('mail::message')
# Monitolite Notification Report
Hello {{ $report['contact']['firstname'] }},
You will find below the full report digest of the Monitolite monitoring application.
@component('mail::table')
| Host | Status | Datetime |
|:------------:|:--------:|:--------:|
@foreach ($report['tasks'] as $t)
@foreach ($t['history'] as $h)
| [{{ $h['task']['host'] }}]({{ $h['task']['host'] }}) | {{ $h['status'] == 1 ? '**UP**': '**DOWN**' }} | {{ date('Y-m-d H:i:s', strtotime($h['created_at'])) }} |
@endforeach
@endforeach
@endcomponent
@component('mail::button', ['url' => $url])
View the dashboard
@endcomponent
Thanks,<br>
{{ config('app.name') }}
@endcomponent

View file

@ -1,9 +1,123 @@
<template>
<div class="container">
<h3>
Task {{ task.id }}
<!-- <p class="context-menu"><img src="/img/menu.svg" width="40" /></p> -->
</h3>
<div>
<div class="container"
v-if="task.id != null"
>
<h1>
<span class="highlight">{{ task.type }}</span> for host <span class="highlight">{{ task.host }}</span>
<!-- <p class="context-menu"><img src="/img/menu.svg" width="40" /></p> -->
</h1>
Show:
<select
v-model="days"
@change="refreshTask"
>
<option value="3">3 days</option>
<option value="7">7 days</option>
<option value="15">15 days</option>
<option value="30">30 days</option>
</select>
<!-- Uptime chart block -->
<div id="chart" class="round">
<h3>Last {{ days }} days uptime</h3>
<div class="block-content">
<apexchart class="graph" v-if="charts.uptime.render" type="bar" height="350" :options="charts.uptime.options" :series="charts.uptime.series"></apexchart>
<p class="no-data" v-else>No chart to display here</p>
</div>
</div>
<!-- Response time chart block -->
<div id="chart" class="round" v-if="task.type == 'http'">
<h3>Last {{ days }} days response time</h3>
<div class="block-content">
<apexchart class="graph" v-if="charts.response.render" type="line" height="350" :options="charts.response.options" :series="charts.response.series"></apexchart>
<p class="no-data" v-else>No chart to display here</p>
</div>
</div>
<!-- History backlog -->
<div class="round">
<h3>Last {{ days }} days history log</h3>
<div class="block-content" v-if="history && Object.keys(history).length > 0">
<p><i>Showing only records where status has changed</i></p>
<table id="tasks_tbl">
<thead>
<tr>
<th width="10%">Status</th>
<th width="10%">Date</th>
<th width="10%">Time</th>
<th width="*">Output</th>
<th width="10%">Duration</th>
</tr>
</thead>
<tbody>
<tr
v-for="h in history"
v-bind:key="h.id"
>
<td :class="statusText(h.status)">
<img :src="'/img/'+statusText(h.status)+'.svg'" width="16" alt="Status" />
</td>
<td>{{ moment(h.created_at).format('YYYY-MM-DD') }}</td>
<td>{{ moment(h.created_at).format('HH:mm:ss') }}</td>
<td>
<span v-if="h.output">
{{ h.output }}
</span>
<span v-else>
<i>No output</i>
</span>
</td>
<td>
<span v-if="h.duration != null">{{ h.duration+'s' }}</span>
<span v-else><i>No duration</i></span>
</td>
</tr>
</tbody>
</table>
</div>
<p class="no-data" v-else>No history to display here</p>
</div>
<!-- Notifications block -->
<div class="round">
<h3>Last {{ days }} days notifications log</h3>
<div class="block-content" v-if="notifications && Object.keys(notifications).length > 0">
<table id="tasks_tbl">
<thead>
<tr>
<th width="10%">Date</th>
<th width="10%">Time</th>
<th width="15">Firstname</th>
<th width="15%">Lastname</th>
<th width="30%">Email</th>
<th width="10%">Type</th>
<th width="10%">Status</th>
</tr>
</thead>
<tbody>
<tr
v-for="n in notifications"
v-bind:key="n.id"
>
<td>{{ moment(n.created_at).format('YYYY-MM-DD') }}</td>
<td>{{ moment(n.created_at).format('HH:mm:ss') }}</td>
<td>{{ n.contact.firstname }}</td>
<td>{{ n.contact.surname }}</td>
<td>{{ n.contact.email }}</td>
<td>{{ n.task_history.status == 1 ? 'UP' : 'DOWN' }}</td>
<td>{{ n.status.toUpperCase() }}</td>
</tr>
</tbody>
</table>
</div>
<p class="no-data" v-else>No notification to display here</p>
</div>
</div>
</div>
</template>
@ -12,17 +126,238 @@
export default{
data: function() {
return {
task: null
task: {
id: null
},
history: null,
notifications: null,
refresh: null,
loader: null,
days: 3,
first_day: null,
charts: {
uptime: {
render: false,
},
response: {
render: false,
}
}
}
},
mounted: function() {
let task_id = this.$route.params.id ?? null
console.log(task_id)
methods: {
statusText: function (status) {
switch (status) {
case 1:
return 'up';
break;
case 0:
return 'down';
break;
default:
return 'unknown';
}
},
refreshTask: function(callback) {
this.$http.post('/api/getTask/'+this.task.id, {
days: this.days
})
.then(response => {
this.task = response.data.task
this.history = response.data.history
this.first_day = new Date(response.data.first_day).getTime();
this.notifications = response.data.notifications
this.refreshUptimeGraph(response.data.stats.uptime)
if (task_id != null) {
this.$http.get('/api/getTask/'+task_id)
.then(response => this.task = response.data)
if (this.task.type == 'http') {
this.refreshResponseTimeGraph(response.data.stats.times)
}
this.loader.hide()
})
.then(() => {
if (this.refresh == null) {
this.refresh = window.setInterval(() => {
this.refreshTask()
}, 10000)
}
})
.catch(error => {
//TODO: do something
})
.then(() => {
this.loader.hide()
})
},
refreshResponseTimeGraph: function(stats) {
let data = [];
let xaxis = [];
for (let date in stats) {
xaxis.push(new Date(date).getTime())
if (stats[date]['count'] > 0) {
data.push(Math.round( (stats[date]['duration'] / stats[date]['count']) * 100) / 100)
}
else {
data.push(0)
}
}
this.charts.response.options = {
xaxis: {
type: 'datetime',
//min: this.first_day,
categories: xaxis,
labels: {
show: true,
rotate: -45,
}
},
yaxis: {
labels: {
formatter: function (value) {
return (Math.round(value * 100) / 100) + "s";
}
}
},
tooltip: {
x: {
format: "dd MMM yyyy"
}
},
chart: {
type: 'line',
height: 350,
stacked: false
},
legend: {
position: 'right',
offsetX: 0,
offsetY: 50
},
dataLabels: {
enabled: true,
},
colors: ['#00955c'],
stroke: {
curve: 'smooth',
},
fill: {
type: 'gradient',
gradient: {
//shade: 'dark',
shadeIntensity: 1,
type: 'vertical',
opacityFrom: 1,
opacityTo: 1,
colorStops: [
{
offset: 20,
color: "#FAD375",
opacity: 1
},
{
offset: 40,
color: "#61DBC3",
opacity: 1
}
]
}
}
}
this.charts.response.series = [{
name: 'Response time',
data: data
}]
this.charts.response.render = true
},
refreshUptimeGraph: function(stats) {
let xaxis = [];
let new_data_a = [];
let new_data_b = [];
for (let date in stats) {
let total = stats[date]['up'] + stats[date]['down']
xaxis.push(new Date(date).getTime())
if (total > 0) {
new_data_a.push( Math.round(stats[date]['up'] / total * 100) )
new_data_b.push( Math.round(stats[date]['down'] / total * 100) )
}
else {
new_data_a.push( 0 )
new_data_b.push( 0 )
}
}
this.charts.uptime.options = {
xaxis: {
type: 'datetime',
min: this.first_day,
categories: xaxis,
tickAmount: 6,
labels: {
show: true,
rotate: -45,
}
},
yaxis: {
labels: {
formatter: function (value) {
return value + "%";
}
}
},
tooltip: {
x: {
format: "yyyy MMM dd"
}
},
chart: {
type: 'bar',
height: 350,
stacked: true,
stackType: '100%'
},
legend: {
position: 'right',
offsetX: 0,
offsetY: 50
},
}
this.charts.uptime.series = [{
name: 'UP',
data: new_data_a,
color: '#00955c'
},
{
name: 'DOWN',
data: new_data_b,
color: '#ef3232'
}]
this.charts.uptime.render = true
},
},
mounted: function() {
this.loader = this.$loading.show()
this.task.id = this.$route.params.id ?? null
if (this.task.id != null) {
this.refreshTask()
}
}
},
beforeRouteLeave(to, from, next) {
clearTimeout(this.refresh);
next();
},
}
</script>
</script>
<style scoped>
</style>

View file

@ -13,10 +13,10 @@
|
*/
$router->group(['prefix' => 'api/'], function () use ($router) {
$router->get('getTasks/', ['uses' => 'ApiController@getTasks']);
$router->get('getTask/{id}', ['uses' => 'ApiController@getTaskDetails']);
$router->patch('toggleTaskStatus/{id}', ['uses' => 'ApiController@toggleTaskStatus']);
$router->group(['prefix' => '/api'], function () use ($router) {
$router->get('/getTasks/', ['uses' => 'ApiController@getTasks']);
$router->post('/getTask/{id}', ['uses' => 'ApiController@getTaskDetails']);
$router->patch('/toggleTaskStatus/{id}', ['uses' => 'ApiController@toggleTaskStatus']);
});
$router->get('/{route:.*}/', function () {

BIN
screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View file

@ -1,104 +0,0 @@
#
# SQL Export
# Created by Querious (300063)
# Created: 19 December 2021 at 10:19:50 CET
# Encoding: Unicode (UTF-8)
#
SET @ORIG_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
SET FOREIGN_KEY_CHECKS = 0;
SET @ORIG_UNIQUE_CHECKS = @@UNIQUE_CHECKS;
SET UNIQUE_CHECKS = 0;
SET @ORIG_TIME_ZONE = @@TIME_ZONE;
SET TIME_ZONE = '+00:00';
SET @ORIG_SQL_MODE = @@SQL_MODE;
SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';
DROP TABLE IF EXISTS `tasks_history`;
DROP TABLE IF EXISTS `notifications`;
DROP TABLE IF EXISTS `tasks`;
DROP TABLE IF EXISTS `groups`;
DROP TABLE IF EXISTS `contacts`;
CREATE TABLE `contacts` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`surname` varchar(200) NOT NULL,
`firstname` varchar(200) NOT NULL,
`email` varchar(250) NOT NULL,
`phone` varchar(20) NOT NULL,
`creation_date` datetime NOT NULL,
`active` int NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3;
CREATE TABLE `groups` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=244 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_unicode_ci;
CREATE TABLE `tasks` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`host` varchar(255) NOT NULL,
`type` enum('ping','http') NOT NULL,
`params` varchar(255) NOT NULL,
`creation_date` datetime NOT NULL,
`frequency` int unsigned NOT NULL,
`last_execution` datetime DEFAULT NULL,
`active` int NOT NULL DEFAULT '0',
`group_id` int unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `host` (`host`,`type`),
KEY `group_id_frgn` (`group_id`),
CONSTRAINT `group_id_frgn` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=147 DEFAULT CHARSET=utf8mb3;
CREATE TABLE `notifications` (
`task_id` int unsigned NOT NULL,
`contact_id` int unsigned NOT NULL,
PRIMARY KEY (`task_id`,`contact_id`),
KEY `contact_id` (`contact_id`),
CONSTRAINT `notifications_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE,
CONSTRAINT `notifications_ibfk_2` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
CREATE TABLE `tasks_history` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`status` int unsigned NOT NULL,
`datetime` datetime NOT NULL,
`output` text CHARACTER SET utf8 COLLATE utf8_general_ci,
`task_id` int unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `task_id` (`task_id`),
CONSTRAINT `tasks_history_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=88 DEFAULT CHARSET=utf8mb3;
SET FOREIGN_KEY_CHECKS = @ORIG_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS = @ORIG_UNIQUE_CHECKS;
SET @ORIG_TIME_ZONE = @@TIME_ZONE;
SET TIME_ZONE = @ORIG_TIME_ZONE;
SET SQL_MODE = @ORIG_SQL_MODE;
# Export Finished: 19 December 2021 at 10:19:50 CET