Add service monitoring and burger menu

This commit is contained in:
Samuel Ortion 2022-08-15 11:42:28 +02:00
parent bd35b4c496
commit e44b8542b0
17 changed files with 515 additions and 117 deletions

View File

@ -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';

View File

@ -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
View 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;
}

View File

@ -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', [
]);
}
}

View 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', [
]);
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -1,5 +1,4 @@
<?php
// src/Controller/AboutController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;

View File

@ -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),
]);
}
@ -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 [];
}

View File

@ -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>

View 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>

View File

@ -2,4 +2,6 @@
{% block content %}
<p>Welcome to BirdNET-stream !</p>
{% include "stats.html.twig" %}
{% include "chart.html.twig" %}
{% endblock %}

View File

@ -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>

View 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 %}

View 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>

View File

@ -0,0 +1,2 @@
<img class="logo" src="/media/logo.svg" alt="BirdNET-stream logo">
<h1>BirdNET-stream</h1>

View File

@ -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 %}