Add service monitoring and burger menu
This commit is contained in:
parent
bd35b4c496
commit
e44b8542b0
@ -7,6 +7,7 @@
|
||||
|
||||
// any CSS you import will output into a single css file (app.css in this case)
|
||||
import './styles/app.css';
|
||||
import './styles/menu.css';
|
||||
|
||||
// start the Stimulus application
|
||||
import './bootstrap';
|
||||
|
@ -2,15 +2,39 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.behind {
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.above {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: lightgray;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/** Align text and center of image */
|
||||
@ -18,7 +42,7 @@ header {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
header img#logo {
|
||||
header img.logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
@ -26,6 +50,7 @@ header img#logo {
|
||||
main {
|
||||
min-height: 100vh;
|
||||
padding: 5em;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
@ -44,33 +69,7 @@ footer a:hover {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
background-color: #333;
|
||||
margin: auto;
|
||||
min-width: 100vw;
|
||||
}
|
||||
|
||||
nav ul li a,
|
||||
.dropdown-button {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 1em 0.5em;
|
||||
}
|
||||
|
||||
nav ul li a.active,
|
||||
nav ul li a:hover {
|
||||
background-color: #999;
|
||||
color: #101010
|
||||
}
|
||||
|
||||
.dropdown-button:hover {
|
||||
/* .dropdown-button:hover {
|
||||
background-color: #900;
|
||||
color: white
|
||||
}
|
||||
@ -78,6 +77,7 @@ nav ul li a:hover {
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
*/
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
@ -96,3 +96,55 @@ canvas {
|
||||
.scientific-name {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dataviz img {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#statuses {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#statuses li {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.status .bullet {
|
||||
border-radius: 50%;
|
||||
background-color: #999;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 0.15em;
|
||||
margin-right: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.status.active .bullet {
|
||||
background-color: #090;
|
||||
}
|
||||
|
||||
.status.inactive .bullet {
|
||||
background-color: #900;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
main {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
z-index: 10;
|
||||
opacity: 100%;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
position: relative;
|
||||
}
|
129
www/assets/styles/menu.css
Normal file
129
www/assets/styles/menu.css
Normal file
@ -0,0 +1,129 @@
|
||||
nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
--nav-width: 20em;
|
||||
}
|
||||
|
||||
.toggler{
|
||||
z-index:2;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hamburger{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
padding: 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hamburger > div{
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: black;
|
||||
height: 2px;
|
||||
width: 75%;
|
||||
transition: all 0.4s ease;
|
||||
color: #000;
|
||||
|
||||
}
|
||||
|
||||
.hamburger > div::before,
|
||||
.hamburger > div::after{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
background: black;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.hamburger > div::after{
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
.toggler:checked + .hamburger > div{
|
||||
background: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.toggler:checked + .hamburger > div::before{
|
||||
top: 0;
|
||||
transform: rotate(45deg);
|
||||
background: black;
|
||||
}
|
||||
|
||||
.toggler:checked + .hamburger > div::after{
|
||||
top: 0;
|
||||
transform: rotate(135deg);
|
||||
background: black;
|
||||
}
|
||||
|
||||
.menu{
|
||||
height: 100vh;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.toggler:checked ~ .menu{
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
background-color: rgba(255, 255, 255, 1)!important;
|
||||
}
|
||||
|
||||
.menu > ul{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
width: 0;
|
||||
height: 100vmax;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
margin-top: 2em;
|
||||
visibility: hidden;
|
||||
background-color: white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.menu > ul > li{
|
||||
list-style: none;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.menu > ul > li > a{
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.toggler:checked ~ .fill {
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: var(--nav-width);
|
||||
}
|
||||
|
||||
.toggler:checked ~ .menu > ul{
|
||||
width: 20em;
|
||||
transition: fit-content .5s ease;
|
||||
opacity: 1;
|
||||
transition: opacity .5s, visibility .5s, height .5s, width .5s;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.toggler:checked ~ .menu > ul > li > a:hover{
|
||||
color: orange;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
// src/Controller/AboutController.php
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
||||
class AboutController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/about", name="about")
|
||||
*/
|
||||
public function about()
|
||||
{
|
||||
return $this->render('about/index.html.twig', [
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
31
www/src/Controller/AuthController.php
Normal file
31
www/src/Controller/AuthController.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
class AuthController extends AbstractController
|
||||
{
|
||||
private Connection $connection;
|
||||
|
||||
/**
|
||||
* @Route("/auth", name="auth")
|
||||
*/
|
||||
public function index(Connection $connection)
|
||||
{
|
||||
return $this->redirectToRoute("login");
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/auth/login", name="login")
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
return $this->render('auth/login.html.twig', [
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,21 +1,78 @@
|
||||
<?php
|
||||
// src/Controller/HomeController.php
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
private Connection $connection;
|
||||
|
||||
/**
|
||||
* @Route("/", name="home")
|
||||
*/
|
||||
public function index()
|
||||
public function index(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
return $this->render('index.html.twig', [
|
||||
"stats" => $this->get_stats(),
|
||||
"charts" => $this->last_chart_generated(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/about", name="about")
|
||||
*/
|
||||
public function about()
|
||||
{
|
||||
return $this->render('about/index.html.twig', [
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
private function get_stats()
|
||||
{
|
||||
$stats = array();
|
||||
$stats["most-recorded-species"] = $this->get_most_recorded_species();
|
||||
$stats["last-detected-species"] = $this->get_last_recorded_species();
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function get_most_recorded_species()
|
||||
{
|
||||
$sql = "SELECT `scientific_name`, `common_name`, COUNT(*) AS contact_count
|
||||
FROM `taxon`
|
||||
INNER JOIN `observation`
|
||||
ON `taxon`.`taxon_id` = `observation`.`taxon_id`
|
||||
ORDER BY `contact_count` DESC LIMIT 1";
|
||||
$stmt = $this->connection->prepare($sql);
|
||||
$result = $stmt->executeQuery();
|
||||
return $result->fetchAllAssociative()[0];
|
||||
}
|
||||
|
||||
private function get_last_recorded_species()
|
||||
{
|
||||
$sql = "SELECT `scientific_name`, `common_name`, `date`, `audio_file`, `confidence`
|
||||
FROM `observation`
|
||||
INNER JOIN `taxon`
|
||||
ON `observation`.`taxon_id` = `taxon`.`taxon_id`
|
||||
ORDER BY `date` DESC LIMIT 1";
|
||||
$stmt = $this->connection->prepare($sql);
|
||||
$result = $stmt->executeQuery();
|
||||
return $result->fetchAllAssociative()[0];
|
||||
}
|
||||
|
||||
private function last_chart_generated() {
|
||||
|
||||
$files = glob($this->getParameter('kernel.project_dir') . '/../var/charts/*.png');
|
||||
usort($files, function($a, $b) {
|
||||
return filemtime($b) - filemtime($a);
|
||||
});
|
||||
$last_chart = basename(array_pop($files));
|
||||
return $last_chart;
|
||||
}
|
||||
|
||||
}
|
73
www/src/Controller/ServicesController.php
Normal file
73
www/src/Controller/ServicesController.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\EventListener\ControllerListener;
|
||||
|
||||
class ServicesController extends AbstractController
|
||||
{
|
||||
|
||||
private $services_available = array("recording", "analyzis", "miner", "plotter");
|
||||
private $allowed_actions = array("start", "stop");
|
||||
|
||||
|
||||
/**
|
||||
* @Route("/service/status", name="service_status")
|
||||
*/
|
||||
public function service_status() {
|
||||
$status = array_map(function($service) {
|
||||
return array(
|
||||
"name" => $service,
|
||||
"status" => $this->systemd_service_status($service)
|
||||
);
|
||||
}, $this->services_available);
|
||||
return $this->render('services/status.html.twig', [
|
||||
'status' => $status
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/service/manage/{action}", name="service_manager")
|
||||
*/
|
||||
public function service_manage($action, $service)
|
||||
{
|
||||
$error = "";
|
||||
if (in_array($action, $this->allowed_actions)) {
|
||||
if (in_array($service, $this->services_available)) {
|
||||
if(($output = $this->manage_systemd_service($action, $service)) != "true") {
|
||||
$error = "Error while managing service";
|
||||
dump($output);
|
||||
}
|
||||
} else {
|
||||
$error .= "Service not found";
|
||||
}
|
||||
} else {
|
||||
$error .= "Action not allowed";
|
||||
}
|
||||
if ($error != "") {
|
||||
return new Response($error, Response::HTTP_BAD_REQUEST);
|
||||
} else {
|
||||
return new Response("OK", Response::HTTP_OK);
|
||||
}
|
||||
}
|
||||
|
||||
private function manage_systemd_service($action, $service)
|
||||
{
|
||||
$command = "./daemon/birdnet_manager.sh ".$action;
|
||||
$workdir = $this->getParameter("kernel.project_dir") . "/../";
|
||||
$command = "cd ".$workdir." && ".$command;
|
||||
echo $command;
|
||||
$output = shell_exec($command);
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function systemd_service_status($service)
|
||||
{
|
||||
$command = "systemctl is-active ".$service;
|
||||
$result = shell_exec($command);
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
// src/Controller/AboutController.php
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
// src/Controller/TodayController.php
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@ -18,12 +17,7 @@ class TodayController extends AbstractController
|
||||
*/
|
||||
public function today(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$date = date('Y-m-d');
|
||||
return $this->render('today/index.html.twig', [
|
||||
"date" => $date,
|
||||
"species" => $this->recorded_species_by_date($date),
|
||||
]);
|
||||
return $this->redirectToRoute("today_species");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,7 +29,7 @@ class TodayController extends AbstractController
|
||||
$date = date('Y-m-d');
|
||||
return $this->render('today/index.html.twig', [
|
||||
"date" => $date,
|
||||
"species" => $this->recorded_species_by_date($date)
|
||||
"results" => $this->recorded_species_by_date($date),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -92,7 +86,7 @@ class TodayController extends AbstractController
|
||||
FROM observation
|
||||
INNER JOIN taxon
|
||||
ON observation.taxon_id = taxon.taxon_id
|
||||
WHERE strftime('%Y-%m-%d', `observation`.`date`) =:date
|
||||
WHERE strftime('%Y-%m-%d', `observation`.`date`) = :date
|
||||
GROUP BY observation.taxon_id";
|
||||
$stmt = $this->connection->prepare($sql);
|
||||
$stmt->bindValue(':date', $date);
|
||||
@ -107,7 +101,7 @@ class TodayController extends AbstractController
|
||||
$stmt = $this->connection->prepare($sql);
|
||||
$stmt->bindValue(':id', $id);
|
||||
$result = $stmt->executeQuery();
|
||||
$taxon = $result->fetchAssociative();
|
||||
$taxon = $result->fetchAllAssociative()[0];
|
||||
if (!$taxon) {
|
||||
return [];
|
||||
}
|
||||
|
@ -18,18 +18,18 @@
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
<header>
|
||||
<img id="logo" src="/media/logo.svg" alt="BirdNET-stream logo">
|
||||
<h1>BirdNET-stream</h1>
|
||||
</header>
|
||||
<div class="above">
|
||||
{% include "menu.html.twig" %}
|
||||
|
||||
</div>
|
||||
<div class="behind">
|
||||
<header></header>
|
||||
<main>
|
||||
{% block content %}
|
||||
<p>Welcome to BirdNET-stream !</p>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% include "footer.html.twig" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
7
www/templates/chart.html.twig
Normal file
7
www/templates/chart.html.twig
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="dataviz">
|
||||
{% if charts is defined and charts | length > 0 %}
|
||||
<img src="/media/charts/{{ charts }}" alt="{{ "Frequency charts" | trans }}">
|
||||
{% else %}
|
||||
<p>{{ "No charts available" | trans }}</p>
|
||||
{% endif %}
|
||||
</div>
|
@ -2,4 +2,6 @@
|
||||
|
||||
{% block content %}
|
||||
<p>Welcome to BirdNET-stream !</p>
|
||||
{% include "stats.html.twig" %}
|
||||
{% include "chart.html.twig" %}
|
||||
{% endblock %}
|
@ -1,4 +1,10 @@
|
||||
<nav class="nav-bar">
|
||||
<nav class="navbar">
|
||||
<input type="checkbox" class="toggler">
|
||||
<div class="hamburger">
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<div class="menu overlay">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
@ -37,4 +43,7 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
{% include "title.html.twig" %}
|
||||
</nav>
|
19
www/templates/services/status.html.twig
Normal file
19
www/templates/services/status.html.twig
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "base.html.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>
|
||||
{{ "Services status" | trans }}
|
||||
</h2>
|
||||
{% if status is defined and status | length > 0 %}
|
||||
<ul id="statuses">
|
||||
{% for service in status %}
|
||||
<li>
|
||||
<span class="name">{{ service["name"] }}</span>
|
||||
<span class="status"><span class="bullet {{ service["status"] }}"></span>{{ service["status"] }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>{{ "No status available" | trans }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
42
www/templates/stats.html.twig
Normal file
42
www/templates/stats.html.twig
Normal file
@ -0,0 +1,42 @@
|
||||
<div id="stats">
|
||||
<h2>Quick Stats</h2>
|
||||
<ul>
|
||||
<li class="most-recorded-species">
|
||||
{{ "Most recorded species" | trans }}:
|
||||
<span class="scientific-name">
|
||||
{{ stats["most-recorded-species"]["scientific_name"] }}
|
||||
</span>
|
||||
(<span class="common_name">{{ stats["most-recorded-species"]["common_name"] }}</span>)
|
||||
{{ "with" | trans }}
|
||||
<span class="observation-count">
|
||||
{{ stats["most-recorded-species"]["contact_count"] }}
|
||||
</span>
|
||||
{{ "contacts" | trans }}.
|
||||
</li>
|
||||
<li class="last-recorded-species">
|
||||
{{ "Last detected species" | trans }}:
|
||||
<span class="scientific-name">
|
||||
{{ stats["last-detected-species"]["scientific_name"] }}
|
||||
</span>
|
||||
(<span class="common_name">{{ stats["last-detected-species"]["common_name"] }}</span>)
|
||||
{{ "with" | trans }}
|
||||
<span class="confidence">
|
||||
{{ stats["last-detected-species"]["confidence"] }}
|
||||
</span>
|
||||
{{ "AI confidence" | trans }}
|
||||
<span class="datetime">
|
||||
{% set date = stats["last-detected-species"]["date"] %}
|
||||
{% if date | date("Y-m-d") == "now" | date("Y-m-d") %}
|
||||
{{ "today" | trans }}
|
||||
{% else %}
|
||||
{{ "on" | trans }}
|
||||
{{ date | format_datetime("full", "none") }}
|
||||
{% endif %}
|
||||
at
|
||||
<span class="time">
|
||||
{{ date | date("H:i") }}
|
||||
</span>
|
||||
</span>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
2
www/templates/title.html.twig
Normal file
2
www/templates/title.html.twig
Normal file
@ -0,0 +1,2 @@
|
||||
<img class="logo" src="/media/logo.svg" alt="BirdNET-stream logo">
|
||||
<h1>BirdNET-stream</h1>
|
@ -11,7 +11,7 @@
|
||||
{% endif %}
|
||||
</h2>
|
||||
{# Display a list of records if any, else, print message #}
|
||||
{% if results[0] is defined and results[0] | length > 0 %}
|
||||
{% if results is defined and results | length > 0 %}
|
||||
<ul>
|
||||
{% for sp in results %}
|
||||
<li class="species">
|
||||
@ -23,5 +23,7 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>{{ "No species detected this day" | trans }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user