Compare commits

..

No commits in common. "main" and "1.0" have entirely different histories.
main ... 1.0

123 changed files with 1671 additions and 28412 deletions

View file

@ -1,15 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2

38
.env.example Normal file → Executable file
View file

@ -1,32 +1,14 @@
APP_NAME=Monitolite DB_TYPE=mysql
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_TIMEZONE=UTC
DB_TIMEZONE="+1:00"
LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=
DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_USER=vagrant
DB_PASSWORD=vagrant
DB_NAME=monitoring
DB_PORT=3306 DB_PORT=3306
DB_DATABASE=homestead SMTP_HOST=localhost
DB_USERNAME=homestead SMTP_USER=
DB_PASSWORD=secret SMTP_PASSWORD=
SMTP_PORT=80
CACHE_DRIVER=file SMTP_SSL=1
QUEUE_CONNECTION=sync MAIL_FROM=axel@monitolite.fr
NB_TRIES=3 NB_TRIES=3
ARCHIVE_DAYS=10 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"

7
.gitignore vendored
View file

@ -1,7 +1,2 @@
/vendor web/vendor/**/*
/.idea
Homestead.json
Homestead.yaml
.env .env
.phpunit.result.cache
/node_modules

View file

@ -1,6 +0,0 @@
php:
preset: laravel
disabled:
- unused_use
js: true
css: true

184
README.md
View file

@ -1,98 +1,86 @@
# MONITOLITE # MONITOLITE
**MonitoLite** is an old project I recently dug up from my archives. I developed this script years ago for my personal needs. **MonitoLite** is an old project I recently dug up from my archives. I developed this script years ago for my personal needs.
I figured it could be useful for others so I **rewrote** and **updated** it from scratch in a modern way. I figured it could be useful for others so here we are.
## What it does ## What it does
**MonitoLite** is a very simple monitoring tool developed in PHP powered by Lumen (by Laravel). It supports : **MonitoLite** is a very simple monitoring tool developed in Perl. It supports :
* **PING monitoring**: sends a `ping` command to the specified host. Raises an alert if the host is down * **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. * **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.
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. It uses a SQL backend for handling the tasks and the status of the tasks.
Tested on MySQL only but should support other SQL-based DBMS.
It uses a SQL backend for handling the tasks and the status of the tasks.
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 `monitolite.pl` 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.
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.
I rewrote a couple of things today to make sure the script still works.
## Demo
## Screenshot
[DEMO](https://monitolite.mabox.eu)
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot.png "Logo")
## Screenshot
### Tasks list with quick preview ## Requirements
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot.png "Logo") * Perl : with DBI, Dotenv, Net::Ping, Email::MIME, Email::Sender::Simple, Email::Sender::Transport::SMTP, LWP::Simple, LWP::UserAgent, LWP::Protocol::https
* a MTA: Postfix, ...
### Task details with graph and history * PHP 7+ (optional): with PDO
* a webserver (optional): Apache, Nginx, ...
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot2.png "Logo") * a Database server: MySQL, other? (untested)
* access to CRON tasks
* possibly `root` access for the `ping` command to run (needs confirmation)
## Requirements
* PHP 7+ with cURL, `exec` command allowed, MySQL extension via PDO ## Installation
* a MTA: Postfix, or an external SMTP ...
* a webserver (optional): Apache, Nginx, ... * clone this repo
* a Database server: MySQL, other? (untested) * install Perl dependencies
* access to CRON tasks * install PHP composer dependencies: `cd ./web && composer install`
* create a Database and import the schema from `sql/create.sql`
## Installation * create your own `.env` file: `cp .env.example .env` and adapt it to your needs
* create a webserver vhost with document root to the `web` directory
* clone this repo * add tasks and contacts into the database (no backend yet)
* install PHP composer dependencies: `cd ./web && composer install` * run the script: `perl monitolite.pl`
* create a Database and import the initial schema using `php artisan migrate` * check the web dashboard for results.
* create your own `.env` file: `cp .env.example .env` and adapt it to your needs * when everything works, you may create a CRON `* * * * * cd <change/this/to/the/correct/path> && /usr/bin/perl monitolite.pl > /dev/null`
* create a webserver vhost with document root to the `public` directory
* add tasks and contacts into the database (no GUI for CRUD yet)
* run the script: `cd /var/www/<your-path> && php artisan monitolite:run` ## Settings
* check the output of the command for results.
* if everything works, you may create a CRON `* * * * * cd /var/www/<your-path> && php artisan monitolite:run > /dev/null` * DB_TYPE=mysql
* DB_HOST=127.0.0.1
* DB_USER=vagrant
## Settings * DB_PASSWORD=vagrant
* DB_NAME=monitoring
* APP_NAME=Monitolite * DB_PORT=3306
* APP_ENV=production * SMTP_HOST=localhost
* APP_KEY=<GENERATE KEY HERE> * SMTP_USER=
* APP_DEBUG=false * SMTP_PASSWORD=
* APP_URL=http://localhost * SMTP_PORT=80
* APP_TIMEZONE=UTC * SMTP_SSL=1
* DB_TIMEZONE="+1:00" * MAIL_FROM=axel@monitolite.fr
* DB_CONNECTION=mysql * NB_TRIES=3
* DB_HOST=127.0.0.1 * ARCHIVE_DAYS=10
* DB_PORT=3306
* DB_DATABASE=homestead ## MORE INFORMATION COMING SOON.
* DB_USERNAME=homestead
* DB_PASSWORD=secret ## TODO
* MAIL_MAILER=smtp
* MAIL_HOST=localhost * Make CRUD possible from the backend for adding tasks and contacts
* MAIL_PORT=25 * Multithreading
* MAIL_USERNAME= * SMS Notifications
* MAIL_PASSWORD= * Better dashboard
* MAIL_ENCRYPTION= * Protected backend with authentication
* MAIL_FROM_ADDRESS=noreply@monitolite.fr * Create an installation script
* MAIL_FROM_NAME="Monitolite" * Raise alert when tasks are not run at the correct frequency (CRON down or other reason)
* NB_TRIES=3 * Set a notification capping limit to prevent many notifications to be sent in case of an up-and-down host
* ARCHIVE_DAYS=10 * Add a notification history log
* Keep track of tasks response time
* Daemonize the script (instead of CRONs)
## 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)
[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

@ -1,51 +0,0 @@
<?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

@ -1,341 +0,0 @@
<?php
namespace App\Console\Commands;
use \Exception;
use App\Models\Task;
use App\Models\TaskHistory;
use App\Models\Notification;
use Illuminate\Console\Command;
class RunMonitoring extends Command
{
private $limit = 50;
private $max_tries = 3;
/**
* The name and signature of the console command.
*
* @var string
*/
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.
*
* @var string
*/
protected $description = 'Executes all the monitoring tasks';
/**
* Storing all the results for output
*/
private $results;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$count = 0;
$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 = 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).' tasks to process. Better get started ...');
$this->newLine();
$bar = $this->output->createProgressBar(count($tasks));
$bar->start();
foreach ($tasks as $task) {
$bar->advance();
// Getting current task last status
$previous_status = $task->status;
try {
switch ($task->type) {
case 'ping':
$result = $this->checkPing($task);
break;
case 'http':
$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
throw new Exception('Unknown type "'.$task->type.'"');
}
$new_status = 1;
$history = $this->saveHistory($task, true, 'success', $result['duration'] ?? null);
}
catch(MonitoringException $e) {
$history = $this->saveHistory($task, false, $e->getMessage());
}
catch(Exception $e) {
//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();
$this->newLine(2);
if (!empty($this->results)) {
$this->table(
['ID', 'Host', 'Type', 'Result', 'Attempts', 'Message'],
$this->results
);
}
}
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
];
return $insert;
}
final private function checkPing(Task $task) {
if (! function_exists('exec') || ! is_callable('exec')) {
throw new MonitoringException('The "exec" command is required');
}
// Different command line for different OS
switch (strtolower(php_uname('s'))) {
case 'darmin':
$cmd = 'ping -n 1 -t 5';
break;
case 'windows':
$cmd = 'ping /n 1 /w 5';
break;
case 'linux':
case 'freebsd':
default:
$cmd = 'ping -c 1 -W 5';
break;
}
// If command failed
if (false === $exec = exec($cmd.' '.$task->host, $output, $code)) {
throw new MonitoringException('Unable to execute ping command');
}
// If command returned a non-zero code
if ($code > 0) {
throw new MonitoringException('Ping task failed ('.$exec.')');
}
// Double check
$output = implode(' ', $output);
// Looking for the 100% package loss output
if (preg_match('~([0-9]{1,3})\.[0-9]{0,2}% +(packet)? +loss~', $output, $matches)) {
if (! empty($matches[1])) {
if (floatval($matches[1]) == 100) {
throw new MonitoringException('Packet loss detected ('.($matches[0] ?? 'n/a').')');
}
}
}
// Else everything is fine
return true;
}
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 => $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)
];
$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 [
'result' => true,
'duration' => $duration
];
}
// We are looking for a string in the page
else {
if (strpos($result, $task->params) !== false) {
return [
'result' => true,
'output' => 'String was found in the page',
'duration' => $duration
];
}
else {
throw new MonitoringException('Cannot find the required string into the page');
}
}
}
throw new MonitoringException(curl_error($ch), curl_errno($ch));
}
}
class MonitoringException extends Exception {}

View file

@ -1,94 +0,0 @@
<?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,156 +0,0 @@
<?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
*/
use Illuminate\Console\Command;
class SyncCustomers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitolite:sync';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronizes all customers\' websites with Monitolite';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (env('CMS_ENABLE_SYNC') != true) {
$this->error('Customers synchronisation is globally disabled.');
return null;
}
$this->line('Starting synchronisation');
$customers = $tasks = $contacts = [];
// Getting active customers
$opts = [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_FAILONERROR => true,
CURLOPT_POSTFIELDS => [
'access' => env('CMS_API_ACCESS'),
'token' => env('CMS_API_TOKEN')
],
CURLOPT_URL => env('CMS_API_URL')
];
$ch = curl_init();
curl_setopt_array($ch, $opts);
if ($result = curl_exec($ch)) {
$hosts = [];
$customers = json_decode($result);
$bar = $this->output->createProgressBar(count($customers));
$bar->start();
// Getting existing tasks
$tasks_flat = [];
$tasks = app('db')->select('SELECT * FROM tasks');
foreach ($tasks as $t) {
$tasks_flat[$t->id] = preg_replace('~^https?://~', '', trim($t->host));
}
// Getting existing contacts
$contacts = app('db')->select('SELECT * FROM contacts');
// Getting existing groups
$groups_flat = [];
$groups = app('db')->select('SELECT * FROM `groups`');
foreach ($groups as $g) {
$groups_flat[$g->id] = $g->name;
}
// First we insert new customers
foreach($customers as $c) {
$bar->advance();
$hosts[] = 'https://'.trim($c->domain);
// Checking group existence
if (empty($groups_flat[$c->id])) {
app('db')->insert('INSERT INTO `groups` (`id`, `name`) VALUE (?, ?)', [ $c->id, $c->name ]);
$groups_flat[$c->id] = $c->name;
}
if (false === array_search(trim($c->domain), $tasks_flat)) {
$ret = app('db')->insert('
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' => 'restovisio.com',
'creation_date' => date('Y-m-d H:i:s'),
'frequency' => 3600,
'active' => 1,
'group_id' => $c->id
]);
if ($ret === true) {
$task_id = app('db')->getPdo()->lastInsertId();
// Inserting contacts
foreach ($contacts as $c) {
app('db')->insert('INSERT INTO contact_task (`task_id`, `contact_id`) VALUES (:task_id, :contact_id)', [
'task_id' => $task_id,
'contact_id' => $c->id
]);
}
}
}
}
$bar->finish();
$this->newLine(2);
$this->line('Checking tasks to delete');
$bar = $this->output->createProgressBar(count($tasks));
$bar->start();
// Then we delete old customers
foreach ($tasks as $t) {
$bar->advance();
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]);
}
}
$bar->finish();
}
}
}

View file

@ -1,50 +0,0 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Laravel\Lumen\Console\Kernel as ConsoleKernel;
use App\Console\Commands\SyncCustomers;
use App\Console\Commands\RunMonitoring;
use App\Console\Commands\SendNotifications;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
SyncCustomers::class,
RunMonitoring::class,
SendNotifications::class
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
/**
* This is for my own needs
* You may safely remove this scheduled task
*/
if (env('CMS_ENABLE_SYNC') == true) {
$schedule->command('monitolite:sync')->hourly();
}
/**
* This is the main monitoring task
*/
$schedule->command('monitolite:run')->everyMinute();
/**
* Send all the notifications
*/
$schedule->command('monitolite:notify')->everyMinute();
}
}

View file

@ -1,10 +0,0 @@
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
abstract class Event
{
use SerializesModels;
}

View file

@ -1,16 +0,0 @@
<?php
namespace App\Events;
class ExampleEvent extends Event
{
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
}

View file

@ -1,54 +0,0 @@
<?php
namespace App\Exceptions;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Throwable $exception
* @return void
*
* @throws \Exception
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*
* @throws \Throwable
*/
public function render($request, Throwable $exception)
{
return parent::render($request, $exception);
}
}

View file

@ -1,172 +0,0 @@
<?php
namespace App\Http\Controllers;
use Exception;
use \Carbon\Carbon;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ApiController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
//
}
public function getTasks() {
$tasks = [];
$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)) {
$group_id = $t->id;
$group_name = 'ungrouped';
}
else {
$group_id = $t->group_id;
$group_name = $t->group_name;
}
if (empty($tasks[$group_id])) {
$tasks[$group_id] = [
'id' => $group_id,
'name' => $group_name,
'tasks' => null
];
}
$tasks[$group_id]['tasks'][$t->id] = $t;
}
return response()->json($tasks);
}
public function getTaskDetails(Request $request, $id) {
$days = ($request->input('days', 15) - 1);
$task = Task::with(['group'])
->findOrFail($id)
;
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) {
$active = $request->input('active', null);
if (is_null($active)) {
throw new ApiException('Invalid parameters');
}
$active = intval($active);
$task = Task::findOrFail($id);
$task->active = $active;
if ($task->save()) {
return response()->json($task);
}
else {
throw new ApiException('Cannot disable this task');
}
}
}
class ApiException extends Exception {}

View file

@ -1,10 +0,0 @@
<?php
namespace App\Http\Controllers;
use Laravel\Lumen\Routing\Controller as BaseController;
class Controller extends BaseController
{
//
}

View file

@ -1,44 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;
class Authenticate
{
/**
* The authentication guard factory instance.
*
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if ($this->auth->guard($guard)->guest()) {
return response('Unauthorized.', 401);
}
return $next($request);
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
class ExampleMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
}

View file

@ -1,24 +0,0 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
abstract class Job implements ShouldQueue
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "queueOn" and "delay" queue helper methods.
|
*/
use InteractsWithQueue, Queueable, SerializesModels;
}

View file

@ -1,31 +0,0 @@
<?php
namespace App\Listeners;
use App\Events\ExampleEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ExampleListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \App\Events\ExampleEvent $event
* @return void
*/
public function handle(ExampleEvent $event)
{
//
}
}

View file

@ -1,48 +0,0 @@
<?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')
])
;
}
}

View file

@ -1,22 +0,0 @@
<?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 = [];
}

View file

@ -1,24 +0,0 @@
<?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

@ -1,40 +0,0 @@
<?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();
}
}
}
}

View file

@ -1,40 +0,0 @@
<?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

@ -1,22 +0,0 @@
<?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

@ -1,29 +0,0 @@
<?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

@ -1,33 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Lumen\Auth\Authorizable;
class User extends Model implements AuthenticatableContract, AuthorizableContract
{
use Authenticatable, Authorizable, HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email',
];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'password',
];
}

View file

@ -1,18 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View file

@ -1,39 +0,0 @@
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Boot the authentication services for the application.
*
* @return void
*/
public function boot()
{
// Here you may define how you wish users to be authenticated for your Lumen
// application. The callback which receives the incoming request instance
// should return either a User instance or null. You're free to obtain
// the User instance via an API token or any other method necessary.
$this->app['auth']->viaRequest('api', function ($request) {
if ($request->input('api_token')) {
return User::where('api_token', $request->input('api_token'))->first();
}
});
}
}

View file

@ -1,19 +0,0 @@
<?php
namespace App\Providers;
use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
\App\Events\ExampleEvent::class => [
\App\Listeners\ExampleListener::class,
],
];
}

35
artisan
View file

@ -1,35 +0,0 @@
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| First we need to get an application instance. This creates an instance
| of the application / container and bootstraps the application so it
| is ready to receive HTTP / Console requests from the environment.
|
*/
$app = require __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(
'Illuminate\Contracts\Console\Kernel'
);
exit($kernel->handle(new ArgvInput, new ConsoleOutput));

View file

@ -1,134 +0,0 @@
<?php
require_once __DIR__.'/../vendor/autoload.php';
(new Laravel\Lumen\Bootstrap\LoadEnvironmentVariables(
dirname(__DIR__)
))->bootstrap();
date_default_timezone_set(env('APP_TIMEZONE', 'UTC'));
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| Here we will load the environment and create the application instance
| that serves as the central piece of this framework. We'll use this
| application as an "IoC" container and router for this framework.
|
*/
$app = new Laravel\Lumen\Application(
dirname(__DIR__)
);
$app->withFacades();
$app->withEloquent();
/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
|
| Now we will register a few bindings in the service container. We will
| register the exception handler and the console kernel. You may add
| your own bindings here if you like or you can make another file.
|
*/
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
/*
|--------------------------------------------------------------------------
| Register Config Files
|--------------------------------------------------------------------------
|
| Now we will register the "app" configuration file. If the file exists in
| your configuration directory it will be loaded; otherwise, we'll load
| the default version. You may register other files below as needed.
|
*/
/**
* 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);
/*
|--------------------------------------------------------------------------
| Register Middleware
|--------------------------------------------------------------------------
|
| Next, we will register the middleware with the application. These can
| be global middleware that run before and after each request into a
| route or middleware that'll be assigned to some specific routes.
|
*/
// $app->middleware([
// App\Http\Middleware\ExampleMiddleware::class
// ]);
// $app->routeMiddleware([
// 'auth' => App\Http\Middleware\Authenticate::class,
// ]);
/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
|
| Here we will register all of the application's service providers which
| are used to bind services into the container. Service providers are
| totally optional, so you are not required to uncomment this line.
|
*/
// $app->register(App\Providers\AppServiceProvider::class);
// $app->register(App\Providers\AuthServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);
$app->register(Illuminate\Mail\MailServiceProvider::class);
/*
|--------------------------------------------------------------------------
| Load The Application Routes
|--------------------------------------------------------------------------
|
| Next we will include the routes file so that they can all be added to
| the application. This will provide all of the URLs the application
| can respond to, as well as the controllers that may handle them.
|
*/
$app->router->group([
'namespace' => 'App\Http\Controllers',
], function ($router) {
require __DIR__.'/../routes/web.php';
});
return $app;

View file

@ -1,41 +0,0 @@
{
"name": "laravel/lumen",
"description": "The Laravel Lumen Framework.",
"keywords": ["framework", "laravel", "lumen"],
"license": "MIT",
"type": "project",
"require": {
"php": "^7.3|^8.0",
"illuminate/mail": "^8.77",
"laravel/lumen-framework": "^8.3.1"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"mockery/mockery": "^1.3.1",
"phpunit/phpunit": "^9.5.10"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"classmap": [
"tests/"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
]
}
}

7847
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,137 +0,0 @@
<?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'),
],
],
];

View file

@ -1,117 +0,0 @@
<?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

@ -1,29 +0,0 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = User::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->name,
'email' => $this->faker->unique()->safeEmail,
];
}
}

View file

@ -1,32 +0,0 @@
<?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

@ -1,36 +0,0 @@
<?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

@ -1,31 +0,0 @@
<?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

@ -1,34 +0,0 @@
<?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

@ -1,36 +0,0 @@
<?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

@ -1,33 +0,0 @@
<?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

@ -1,42 +0,0 @@
<?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

@ -1,34 +0,0 @@
<?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

@ -1,34 +0,0 @@
<?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

@ -1,32 +0,0 @@
<?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

@ -1,32 +0,0 @@
<?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,18 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// $this->call('UsersTableSeeder');
}
}

350
monitolite.pl Normal file
View file

@ -0,0 +1,350 @@
#!/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_TYPE'};
my $hostname = $ENV{'DB_HOST'};
my $database = $ENV{'DB_NAME'};
my $login = $ENV{'DB_USER'};
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);
}
}
}
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 => 0 },
protocols_allowed => ['http', 'https']
);
$check->timeout(5);
$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) = @_;
my $query = $dbh->prepare('INSERT INTO tasks_history (status, datetime, task_id) VALUES(' . $status . ', "'.$datetime.'", ' . $task_id . ')');
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 = 'ALERT: 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 = 'RECOVERY: 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;
}

16852
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
{
"devDependencies": {
"laravel-mix": "^6.0.39",
"resolve-url-loader": "^4.0.0",
"sass": "^1.45.0",
"sass-loader": "^12.4.0",
"vue-loader": "^15.9.8",
"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,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
</php>
</phpunit>

View file

@ -1,21 +0,0 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

View file

@ -1,4 +0,0 @@
@import url(https://fonts.googleapis.com/css2?family=Hind:wght@300;400;500;600;700&display=swap);
@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)}}
/*# sourceMappingURL=app.css.map*/

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title/><g data-name="Layer 51" id="Layer_51"><path d="M16,2A14,14,0,1,0,30,16,14,14,0,0,0,16,2ZM4,16A11.89,11.89,0,0,1,6.85,8.26L23.74,25.15A12,12,0,0,1,4,16Zm21.15,7.74L8.26,6.85A12,12,0,0,1,25.15,23.74Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 300 B

View file

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M22 15h-3V3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-5.293 1.293l-6.4 6.4a.5.5 0 0 1-.654.047L8.8 22.1a1.5 1.5 0 0 1-.553-1.57L9.4 16H3a2 2 0 0 1-2-2v-2.104a2 2 0 0 1 .15-.762L4.246 3.62A1 1 0 0 1 5.17 3H16a1 1 0 0 1 1 1v11.586a1 1 0 0 1-.293.707z"/></g></svg>

Before

Width:  |  Height:  |  Size: 385 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M4 20v-6a8 8 0 1 1 16 0v6h1v2H3v-2h1zm2-6h2a4 4 0 0 1 4-4V8a6 6 0 0 0-6 6zm5-12h2v3h-2V2zm8.778 2.808l1.414 1.414-2.12 2.121-1.415-1.414 2.121-2.121zM2.808 6.222l1.414-1.414 2.121 2.12L4.93 8.344 2.808 6.222z"/></g></svg>

Before

Width:  |  Height:  |  Size: 352 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8z"/></g></svg>

Before

Width:  |  Height:  |  Size: 273 B

View file

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 989 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg id="Layer_1" style="enable-background:new 0 0 128 128;" version="1.1" viewBox="0 0 128 128" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><path d="M64,126c34.2,0,62-27.8,62-62S98.2,2,64,2S2,29.8,2,64S29.8,126,64,126z M16,88.7l25.2-0.2c2.8,10.1,7.5,19.9,13.9,28.7 C38,114.4,23.7,103.5,16,88.7z M47.6,47H79c2.3,11,2.3,22.3,0.2,33.3l-31.6,0.2C45.3,69.4,45.3,58,47.6,47z M63.3,114.9 c-6.3-8.1-10.9-17-13.7-26.4l27.5-0.2C74.2,97.7,69.6,106.7,63.3,114.9z M71.3,117.5c6.6-9,11.3-18.9,14.1-29.3l26.9-0.2 C104.5,103.7,89.3,115,71.3,117.5z M118,64c0,5.6-0.9,11-2.4,16l-28.3,0.2c2-11,1.9-22.2-0.2-33.2h28.1C117,52.3,118,58.1,118,64z M111.8,39H85.2c-2.9-10-7.5-19.7-13.9-28.5C89,12.9,103.9,23.8,111.8,39z M76.9,39H49.7c2.9-9.2,7.4-17.9,13.6-25.9 C69.5,21.1,74,29.8,76.9,39z M55.1,10.8C48.8,19.5,44.2,29,41.4,39H16.2C23.9,24.3,38.1,13.6,55.1,10.8z M39.5,47 c-2.1,11.1-2.1,22.4-0.1,33.5l-26.7,0.2C10.9,75.4,10,69.8,10,64c0-5.9,1-11.7,2.8-17H39.5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z"/></g></svg>

Before

Width:  |  Height:  |  Size: 248 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 14c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-7c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g></svg>

Before

Width:  |  Height:  |  Size: 290 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M6 12C4.89543 12 4 11.1046 4 10C4 8.89543 4.89543 8 6 8C7.10457 8 8 8.89543 8 10C8 11.1046 7.10457 12 6 12Z" fill="#212121"/><path d="M18 10C18 7.79086 16.2091 6 14 6H6C3.79086 6 2 7.79086 2 10C2 12.2091 3.79086 14 6 14H14C16.2091 14 18 12.2091 18 10ZM14 7C15.6569 7 17 8.34315 17 10C17 11.6569 15.6569 13 14 13H6C4.34315 13 3 11.6569 3 10C3 8.34315 4.34315 7 6 7H14Z" fill="#212121"/></svg>

Before

Width:  |  Height:  |  Size: 517 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 7C4.23858 7 2 9.23858 2 12C2 14.7614 4.23858 17 7 17H17C19.7614 17 22 14.7614 22 12C22 9.23858 19.7614 7 17 7H7ZM16.75 14.5C15.3693 14.5 14.25 13.3807 14.25 12C14.25 10.6193 15.3693 9.5 16.75 9.5C18.1307 9.5 19.25 10.6193 19.25 12C19.25 13.3807 18.1307 14.5 16.75 14.5Z" fill="#212121"/></svg>

Before

Width:  |  Height:  |  Size: 422 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg enable-background="new 0 0 32 32" id="Layer_4" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><polygon fill="none" points="12,3 12,8 31,8 31,14 1,14 " stroke="#000000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/><polygon fill="none" points="20,29 20,24 1,24 1,18 31,18 " stroke="#000000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/></g></svg>

Before

Width:  |  Height:  |  Size: 508 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M1.181 12C2.121 6.88 6.608 3 12 3c5.392 0 9.878 3.88 10.819 9-.94 5.12-5.427 9-10.819 9-5.392 0-9.878-3.88-10.819-9zM12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm0-2a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></g></svg>

Before

Width:  |  Height:  |  Size: 330 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></g></svg>

Before

Width:  |  Height:  |  Size: 210 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M4 8h16v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8zm2 2v10h12V10H6zm3 2h2v6H9v-6zm4 0h2v6h-2v-6zM7 5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v2h5v2H2V5h5zm2-1v1h6V4H9z"/></g></svg>

Before

Width:  |  Height:  |  Size: 294 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm2-1.645A3.502 3.502 0 0 0 12 6.5a3.501 3.501 0 0 0-3.433 2.813l1.962.393A1.5 1.5 0 1 1 12 11.5a1 1 0 0 0-1 1V14h2v-.645z"/></g></svg>

Before

Width:  |  Height:  |  Size: 354 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M2 9h3v12H2a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1zm5.293-1.293l6.4-6.4a.5.5 0 0 1 .654-.047l.853.64a1.5 1.5 0 0 1 .553 1.57L14.6 8H21a2 2 0 0 1 2 2v2.104a2 2 0 0 1-.15.762l-3.095 7.515a1 1 0 0 1-.925.619H8a1 1 0 0 1-1-1V8.414a1 1 0 0 1 .293-.707z"/></g></svg>

Before

Width:  |  Height:  |  Size: 383 B

View file

@ -1 +0,0 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></g></svg>

Before

Width:  |  Height:  |  Size: 247 B

View file

@ -1,28 +0,0 @@
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| First we need to get an application instance. This creates an instance
| of the application / container and bootstraps the application so it
| is ready to receive HTTP / Console requests from the environment.
|
*/
$app = require __DIR__.'/../bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$app->run();

File diff suppressed because one or more lines are too long

View file

@ -1,29 +0,0 @@
/*!
* 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

File diff suppressed because one or more lines are too long

View file

@ -1,4 +0,0 @@
{
"/js/app.js": "/js/app.js",
"/css/app.css": "/css/app.css"
}

View file

@ -1,86 +0,0 @@
//window.Vue = require('vue')
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import axios from 'axios'
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'
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/task/:id',
name: 'taskdetails',
component: TaskDetails,
},
],
});
const store = new Vuex.Store({
state: {
tasks: null
},
mutations: {
setTasks(state, tasks) {
state.tasks = tasks
},
updateTask(state, update) {
let tasks = state.tasks
if (
tasks.hasOwnProperty(update.group_id) &&
tasks[update.group_id].hasOwnProperty('tasks') &&
tasks[update.group_id]['tasks'].hasOwnProperty(update.id)
) {
tasks[update.group_id]['tasks'][update.id] = update;
}
}
}
})
var runApp = function() {
new Vue({
router,
components: { Home },
store,
}).$mount('#app')
}
window.addEventListener('load', function () {
runApp();
})

View file

@ -1,280 +0,0 @@
@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
**/
$bg_color: #0a9f9a;
$up_color: #8adf8a;
$down_color: #f79292;
$unknown_color: rgb(245, 214, 158);
$inactive_color: #dfdfdf;
* {
padding: 0;
margin: 0;
}
html {
font-size: 100%;
scroll-behavior: smooth;
body {
padding: 10px;
font-family: 'Hind', sans-serif;
font-size: 1rem;
color: #3D3D3D;
background-image: url(../img/bush.png);
background-attachment: fixed;
a, a:visited {
color: inherit;
text-decoration: inherit;
&:hover {
text-decoration: underline;
}
}
.container {
padding: 0;
margin: 0 auto;
max-width: 1000px;
}
h1, h2, h3, h4 {
margin-top: .8rem;
margin-bottom: .8rem;
}
h1 {
font-size: 2.4rem;
text-align: center;
margin: 3rem 0;
}
h2 {
font-size: 1.4rem;
margin-top: 0;
padding-top: 0;
}
h3 {
font-size: 1.4rem;
background-color: $bg_color;
margin: 0;
padding: .8rem;
color: rgb(240, 240, 240);
position: relative;
small {
font-size: .9rem;
}
.context-menu {
position: absolute;
right: .7rem;
top: .7rem;
cursor: pointer;
font-size: 1rem;
}
}
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);
border-radius: 5px;
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;
display: inline-block;
border-radius: .4rem;
overflow: hidden;
cursor: pointer;
.square {
height: 100%;
margin: 0;
text-align: center;
vertical-align: middle;
float: left;
line-height: 1.2rem;
min-width: 1.4rem;
padding: .2rem .6rem;
&:not(:first-of-type) {
border-left: 1px solid white;
}
}
}
}
.tasks {
.task {
margin-top: 2rem;
padding: 0;
-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);
border-radius: 5px;
position: relative;
background-color: white;
overflow: hidden;
}
}
.spacer {
clear:both;
line-height: 0;
padding: 0;
margin:0;
}
.block-content {
padding: .8rem;
}
.highlight {
background-color: #166260;
padding: 0px 1rem;
display: inline-block;
color: #FFF;
vertical-align: middle;
border-radius: .5rem;
font-size: 1rem;
}
.small {
font-size: .8rem;
}
.hidden {
display: none;
}
.up {
background-color: $up_color;
}
.down {
background-color: $down_color;
}
.unknown {
background-color: $unknown_color;
}
.inactive {
background-color: $inactive_color !important;
opacity: 0.5;
}
.refreshed-time {
text-align: right;
font-size: .8rem;
margin-bottom: 2rem;
.clock {
font-family: Digital7;
font-size: 1.2rem;
background-color: #000;
border-radius: 4px;
color: #FFF;
padding: .3rem .5rem;
}
}
}
}
@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);
}
}

View file

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<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="{{ url('js/app.js') }}"></script>
<link type="text/css" rel="stylesheet" href="{{ url('css/app.css') }}" />
</head>
<body>
<div id="app">
<router-view></router-view>
</div>
</body>
</html>

View file

@ -1,64 +0,0 @@
<template>
<div class="container">
<h1>MonitoLite Dashboard</h1>
<p class="refreshed-time">Last refresh: <br /><span class="clock">{{ refreshedTime }}</span></p>
<quick-view></quick-view>
<task-list></task-list>
</div>
</template>
<script>
import TaskList from './components/tasklist.vue'
import QuickView from './components/quickview.vue'
export default{
components: {
QuickView,
TaskList,
},
data: function() {
return {
refreshed_time: null,
refresh: null,
loading: true,
color: '#FF0000',
size: '10rem',
}
},
computed: {
refreshedTime: function() {
return this.refreshed_time != null ? this.moment(this.refreshed_time).format('HH:mm:ss') : 'never'
}
},
methods: {
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()
})
}
},
beforeRouteLeave(to, from, next) {
clearTimeout(this.refresh)
next();
},
mounted: function() {
this.loading = this.$loading.show()
this.getTasks()
this.refresh = window.setInterval(() => {
this.getTasks();
}, 60000)
}
}
</script>
<style scoped>
</style>

View file

@ -1,24 +0,0 @@
<template>
<div>
<form
v-on:submit.prevent="addTask"
>
<button>Add task</button>
</form>
</div>
</template>
<script>
export default {
props: [
'tasks'
],
methods: {
addTask: function() {
this.$http.post('api.php?a=add_task')
}
}
}
</script>

View file

@ -1,65 +0,0 @@
<template>
<div class="quick-view round">
<h3>
Quick overview
</h3>
<div class="block-content">
<div
v-if="tasks && Object.keys(tasks).length > 0"
>
<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>
</div>
</div>
</template>
<script>
export default {
computed: {
tasks: function() {
return this.$store.state.tasks
}
},
methods: {
statusText: function (status) {
switch (status) {
case 1:
return 'up';
break;
case 0:
return 'down';
break;
default:
return 'unknown';
}
},
}
}
</script>

View file

@ -1,116 +0,0 @@
<template>
<div class="tasks">
<div
v-for="group in tasks"
v-bind:key="group.id"
class="task round"
>
<a :name="'group-'+group.id"></a>
<h3>
Tasks for <span class="highlight">{{ group.name }} <small>(#{{ group.id }})</small></span>
<!-- <p class="context-menu"><img src="/img/menu.svg" width="40" /></p> -->
</h3>
<div class="block-content">
<table id="tasks_tbl">
<thead>
<tr>
<th width="5%">Up?</th>
<th width="*">Host</th>
<th width="10%">Type</th>
<th width="20%">Last checked</th>
<th width="13%">Frequency (min)</th>
<th width="5%">Active</th>
<th width="5%">Actions</th>
</tr>
</thead>
<tbody>
<tr
v-for="task in group.tasks"
v-bind:key="task.id"
:class="task.active == 0 ? 'inactive' : ''"
>
<td :class="statusText(task.status)">
<img :src="'/img/'+statusText(task.status)+'.svg'" width="16" alt="Status" />
</td>
<td>
<img src="/img/external.svg" alt="View host" width="16">
<a :href="task.host" target="_blank">{{ task.host }}</a>
</td>
<td>
<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.executed_at"
>
{{ moment(task.executed_at).fromNow() }}
</span>
<span
v-else
>
Never
</span>
<td>{{ task.frequency / 60 }}</td>
<td :class="task.active == 0 ? 'inactive' : ''">
<a
v-on:click.prevent="disableTask(task.id, task.active)"
href="#"
:title="task.active == 1 ? 'Disable task' : 'Enable task'"
>
<img :src="task.active == 1 ? '/img/on.svg' : '/img/off.svg'" alt="Disable" width="24" />
</a>
</td>
<td>
<router-link :to="{ name: 'taskdetails', params: { id: task.id }}">
<img src="/img/see.svg" alt="Details" width="20" />
</router-link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
components: {
},
computed: {
tasks: function() {
return this.$store.state.tasks
},
},
methods: {
statusText: function (status) {
switch (status) {
case 1:
return 'up';
break;
case 0:
return 'down';
break;
default:
return 'unknown';
}
},
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(() => {
this.loading.hide()
})
}
}
}
</script>

View file

@ -1,24 +0,0 @@
@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,363 +0,0 @@
<template>
<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>
<script>
export default{
data: function() {
return {
task: {
id: null
},
history: null,
notifications: null,
refresh: null,
loader: null,
days: 3,
first_day: null,
charts: {
uptime: {
render: false,
},
response: {
render: false,
}
}
}
},
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 (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>
<style scoped>
</style>

View file

@ -1,24 +0,0 @@
<?php
/** @var \Laravel\Lumen\Routing\Router $router */
/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/
$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 () {
return View('app');
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

114
sql/create.sql Normal file
View file

@ -0,0 +1,114 @@
-- MySQL dump 10.13 Distrib 5.1.37, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: monitoring
-- ------------------------------------------------------
-- Server version 5.1.37-1ubuntu5
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Current Database: `monitoring`
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `monitoring` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci */;
USE `monitoring`;
--
-- Table structure for table `contacts`
--
DROP TABLE IF EXISTS `contacts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `contacts` (
`id` int(11) 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(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `notifications`
--
DROP TABLE IF EXISTS `notifications`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `notifications` (
`task_id` int(11) unsigned NOT NULL,
`contact_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`task_id`,`contact_id`),
KEY `contact_id` (`contact_id`),
CONSTRAINT `notifications_ibfk_2` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE,
CONSTRAINT `notifications_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tasks`
--
DROP TABLE IF EXISTS `tasks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tasks` (
`id` int(11) 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(10) unsigned NOT NULL,
`last_execution` datetime NULL,
`active` int(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `host` (`host`,`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tasks_history`
--
DROP TABLE IF EXISTS `tasks_history`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tasks_history` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`status` int(1) unsigned NOT NULL,
`datetime` datetime NOT NULL,
`task_id` int(11) 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 NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2010-03-04 16:41:51

View file

@ -1,2 +0,0 @@
*
!.gitignore

View file

@ -1,3 +0,0 @@
*
!data/
!.gitignore

View file

@ -1,2 +0,0 @@
*
!.gitignore

Some files were not shown because too many files have changed in this diff Show more