Erster Commit
This commit is contained in:
parent
3b9e7bb194
commit
ce5baaac35
96
README.md
96
README.md
@ -1,3 +1,95 @@
|
|||||||
# plweb
|
# PlusLifeWeb
|
||||||
|
|
||||||
|
Dieses kleine PHP-Projekt bietet die Möglichkeit, Tests, die mit einem
|
||||||
|
[Pluslife-Gerät](https://altruan.de/pages/pluslife-pcr) und dem
|
||||||
|
[Pluslife Analyzer](https://virus.sucks/) durchgeführt wurden, zu speichern
|
||||||
|
und später nochmals anzuschauen.
|
||||||
|
|
||||||
|
Außerdem können Benachrichtigungen via Webhook an [Home Assistant](https://www.home-assistant.io/)
|
||||||
|
geschickt werden, um diese weiter zu verarbeiten.
|
||||||
|
|
||||||
|
Hierfür wurden folgende Komponenten benutzt:
|
||||||
|
|
||||||
|
* [Pluslife Analyzer Displayer von sistason](https://github.com/sistason/pluslife_analyzer_displayer/tree/main):
|
||||||
|
Dieses Projekt war der Ausgangspunkt für PlusLifeWeb
|
||||||
|
* jQuery
|
||||||
|
* Bootstrap
|
||||||
|
* Twig
|
||||||
|
* Chart.js
|
||||||
|
* PostgreSQL als Datenbank
|
||||||
|
|
||||||
|
**Wichtig:**
|
||||||
|
Es wird dringend davon abgeraten, PlusLifeWeb öffentlich zugänglich zu machen, da keinerlei Authentifizierung
|
||||||
|
implementiert ist! Jede Person, die PlusLifeWeb aufrufen kann, kann die Testergebnisse anschauen und auch
|
||||||
|
neue per Webhook hinzufügen!
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
PlusLifeWeb wurde unter Devuan 12 "Bookworm" entwickelt und getestet. Daher sollte es auch unter
|
||||||
|
Debian 12 funktionieren.
|
||||||
|
|
||||||
|
Grundsätzlich wird ein Webserver, z.B. Apache, benötigt.
|
||||||
|
Die Verbindung zum Webserver muß per HTTPS möglich sein, damit der Webhook von virus.sucks funktioniert.
|
||||||
|
|
||||||
|
Folgende PHP-Pakete werden benötigt:
|
||||||
|
|
||||||
|
* php-pgsql
|
||||||
|
* php-twig
|
||||||
|
* php-json
|
||||||
|
* php-curl
|
||||||
|
|
||||||
|
Zur Installation genügt es, die Dateien in einem Webserver-Verzeichnis abzulegen, in dem PHP funktioniert.
|
||||||
|
|
||||||
|
### Datenbank
|
||||||
|
|
||||||
|
In PostgreSQL wird eine Datenbanktabelle für die Tests benötigt.
|
||||||
|
|
||||||
|
Die Datenbank wird als Benutzer *postgres* angelegt (Als root: `su - postgres`).
|
||||||
|
|
||||||
|
```
|
||||||
|
# createdb plweb
|
||||||
|
```
|
||||||
|
|
||||||
|
Anschließend, weiterhin als Benutzer postgres, das Kommando `psql` aufrufen:
|
||||||
|
|
||||||
|
```
|
||||||
|
# psql plweb
|
||||||
|
```
|
||||||
|
|
||||||
|
Mit folgenden SQL-Kommandos wird dann die Tabelle sowie ein Benutzer angelegt:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE tests (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
timestamp TIMESTAMP NOT NULL,
|
||||||
|
data JSONB
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE USER plweb WITH ENCRYPTED PASSWORD 'plweb';
|
||||||
|
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE PLWEB TO plweb;
|
||||||
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA PUBLIC TO plweb;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA PUBLIC TO plweb;
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Paßwort ist hier nur *plweb* - es sollte besser ein anderes benutzt werden.
|
||||||
|
|
||||||
|
### Konfiguration
|
||||||
|
|
||||||
|
In der Datei `config.php` können folgende Parameter eingestellt werden:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$hawebhook = "http://homeassistant:8123/webhook/plweb";
|
||||||
|
$dbconnstring = "host=localhost dbname=plweb user=plweb password=plweb";
|
||||||
|
$test_url = "https://example.com/plweb/test.php?";
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Variable `hawebhook` enthält die URL des Home Assistant-Webhooks, so wie dieser
|
||||||
|
von PlusLifeWeb aus (serverseitig) aufgerufen wird.
|
||||||
|
|
||||||
|
Die Datenbankkonfiguration ist in `dbconnstring` enthalten. Hier ggf. das Paßwort anpassen.
|
||||||
|
|
||||||
|
Die `test_url` zeigt zum Script test.php der PlusLifeWeb-Installation.
|
||||||
|
Diese URL wird, zusammen mit der ID des fertigen Tests, als Benachrichtigung an Home
|
||||||
|
Assistant übertragen.
|
||||||
|
|
||||||
PlusLifeWeb
|
|
7
config.php
Normal file
7
config.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$hawebhook = "http://homeassistant:8123/webhook/plweb"; // Webhook-URL von Home Assistant
|
||||||
|
$dbconnstring = "host=localhost dbname=plweb user=plweb password=plweb";
|
||||||
|
$test_url = "https://example.com/plweb/test.php?";
|
||||||
|
|
||||||
|
?>
|
6
css/bootstrap.min.css
vendored
Normal file
6
css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
css/bootstrap.min.css.map
Normal file
1
css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
37
doc/HomeAssistant.md
Normal file
37
doc/HomeAssistant.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
```
|
||||||
|
alias: Webhook Pluslife
|
||||||
|
mode: single
|
||||||
|
triggers:
|
||||||
|
- trigger: webhook
|
||||||
|
allowed_methods:
|
||||||
|
- POST
|
||||||
|
local_only: true
|
||||||
|
webhook_id: plweb
|
||||||
|
conditions: []
|
||||||
|
actions:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{% if trigger.json.url is defined %}true{% endif %}"
|
||||||
|
then:
|
||||||
|
- action: notify.familie
|
||||||
|
metadata: {}
|
||||||
|
data:
|
||||||
|
message: |-
|
||||||
|
{{ trigger.json.message }}
|
||||||
|
Klicken, um den Test anzusehen.
|
||||||
|
data:
|
||||||
|
clickAction: "{{ trigger.json.url }}"
|
||||||
|
group: plweb
|
||||||
|
title: PlusLifeWeb
|
||||||
|
else:
|
||||||
|
- action: notify.familie
|
||||||
|
metadata: {}
|
||||||
|
data:
|
||||||
|
message: "{{ trigger.json.message }}"
|
||||||
|
title: PlusLifeWeb
|
||||||
|
data:
|
||||||
|
group: plweb
|
||||||
|
```
|
||||||
|
|
||||||
|
|
42
functions.php
Normal file
42
functions.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An example CORS-compliant method. It will allow any GET, POST, or OPTIONS requests from any
|
||||||
|
* origin.
|
||||||
|
*
|
||||||
|
* In a production environment, you probably want to be more restrictive, but this gives you
|
||||||
|
* the general idea of what is involved. For the nitty-gritty low-down, read:
|
||||||
|
*
|
||||||
|
* - https://developer.mozilla.org/en/HTTP_access_control
|
||||||
|
* - https://fetch.spec.whatwg.org/#http-cors-protocol
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function cors() {
|
||||||
|
// Allow from any origin
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
header('Access-Control-Max-Age: 86400'); // cache for 1 day
|
||||||
|
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
|
||||||
|
header("Access-Control-Request-Headers: content-type");
|
||||||
|
// Access-Control headers are received during OPTIONS requests
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
||||||
|
// header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
|
||||||
|
// header("Access-Control-Request-Headers: content-type");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function return_success() {
|
||||||
|
return json_encode(['status' => 'success', 'message' => 'OK']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function return_error($code, $msg) {
|
||||||
|
http_response_code($code);
|
||||||
|
return json_encode(['status' => 'error', 'message' => $msg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function twig_error($twig, $code, $msg) {
|
||||||
|
http_response_code($code);
|
||||||
|
return $twig->render('error.html.twig', [ 'error' => $msg ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
27
index.php
Normal file
27
index.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'Twig/autoload.php';
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
require_once 'functions.php';
|
||||||
|
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
|
$loader = new FilesystemLoader(__DIR__ . '/templates');
|
||||||
|
$twig = new Environment($loader);
|
||||||
|
|
||||||
|
// --------------
|
||||||
|
|
||||||
|
$dbconn = pg_connect($dbconnstring)
|
||||||
|
or die(twig_error($twig, 'Kann Verbindung zur Datenbank nicht herstellen.'));
|
||||||
|
|
||||||
|
$query = "SELECT id,timestamp FROM tests ORDER BY timestamp DESC";
|
||||||
|
$result = pg_query($dbconn, $query)
|
||||||
|
or die(twig_error($twig, 'Datenbankfehler: ' . pg_last_error()));
|
||||||
|
|
||||||
|
$tests = pg_fetch_all($result);
|
||||||
|
pg_close($dbconn);
|
||||||
|
|
||||||
|
echo $twig->render('index.html.twig', [ 'tests' => $tests ]);
|
||||||
|
|
||||||
|
?>
|
7
js/bootstrap.min.js
vendored
Normal file
7
js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
js/bootstrap.min.js.map
Normal file
1
js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
14
js/chart.umd.js
Normal file
14
js/chart.umd.js
Normal file
File diff suppressed because one or more lines are too long
1
js/chart.umd.js.map
Normal file
1
js/chart.umd.js.map
Normal file
File diff suppressed because one or more lines are too long
2
js/jquery.min.js
vendored
Normal file
2
js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
115
js/pluslife.js
Normal file
115
js/pluslife.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
var chart;
|
||||||
|
var channel_colors = {
|
||||||
|
"0": {"color": "#a6cee3", "name": "Channel 1"},
|
||||||
|
"1": {"color": "#1f78b4", "name": "Channel 2"},
|
||||||
|
"2": {"color": "#b2df8a", "name": "Channel 3"},
|
||||||
|
"3": {"color": "#33a02c", "name": "Control Channel (4)"},
|
||||||
|
"4": {"color": "#fb9a99", "name": "Channel 5"},
|
||||||
|
"5": {"color": "#e31a1c", "name": "Channel 6"},
|
||||||
|
"6": {"color": "#fdbf6f", "name": "Channel 7"}
|
||||||
|
};
|
||||||
|
|
||||||
|
var channel_time_data = {
|
||||||
|
"0": new Map(),
|
||||||
|
"1": new Map(),
|
||||||
|
"2": new Map(),
|
||||||
|
"3": new Map(),
|
||||||
|
"4": new Map(),
|
||||||
|
"5": new Map(),
|
||||||
|
"6": new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
var labels = [];
|
||||||
|
var datasets = {};
|
||||||
|
for (const [channel, time_values] of Object.entries(channel_time_data)) {
|
||||||
|
datasets[channel] = {
|
||||||
|
label: channel_colors[channel].name,
|
||||||
|
borderColor: channel_colors[channel].color,
|
||||||
|
fill: false,
|
||||||
|
cubicInterpolationMode: 'monotone',
|
||||||
|
tension: 0.4,
|
||||||
|
channel: channel,
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
//TODO: gaps?
|
||||||
|
for (const [time, value] of time_values.entries()) {
|
||||||
|
if (! labels.includes(time)) labels.push(time);
|
||||||
|
datasets[channel].data.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: Object.values(datasets)
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
aspectRatio: 2,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
boxWidth: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Test Time [min:sec]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: true,
|
||||||
|
//suggestedMin: -10,
|
||||||
|
//suggestedMax: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function update_chart(obj){
|
||||||
|
for (const [channel, data] of Object.entries(obj)) {
|
||||||
|
for (var [time, value] of Object.entries(data)) {
|
||||||
|
time = Number(time); // js json keys are always strings...
|
||||||
|
channel_time_data[channel].set(time, value);
|
||||||
|
|
||||||
|
if (! chart.data.labels.includes(time)) {
|
||||||
|
chart.data.labels.push(time);
|
||||||
|
var progress_html = document.getElementById('status').getElementsByTagName('progress')[0];
|
||||||
|
|
||||||
|
time_remaining = 35*60 - time;
|
||||||
|
minutes_remaining = (time_remaining/60).toFixed(0);
|
||||||
|
seconds_remaining = (time_remaining%60);
|
||||||
|
human_readable_remaining = `${minutes_remaining}:${seconds_remaining} min`;
|
||||||
|
|
||||||
|
progress_html.innerHTML = human_readable_remaining;
|
||||||
|
progress_html.value = time;
|
||||||
|
document.getElementById('timeremaining').innerHTML = `${human_readable_remaining} remaining`;
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.data.datasets.forEach((dataset) => {
|
||||||
|
if (dataset.channel == channel)
|
||||||
|
dataset.data.push(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj) chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
chart = new Chart(document.getElementById('data'), config);
|
||||||
|
})();
|
||||||
|
|
98
js/plweb.js
Normal file
98
js/plweb.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
function get_human_readable_time(time){
|
||||||
|
minutes = (time > 60) ? Math.floor(time/60) : 0;
|
||||||
|
seconds = (time%60).toFixed(0);
|
||||||
|
return `${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function result_text(result) {
|
||||||
|
const result_enum = {1: 'Negative', 2: 'Positive', 3: 'Invalid'};
|
||||||
|
if (Number.isInteger(result)) return result_enum[Number(result)];
|
||||||
|
else return result.toLowerCase().replace(/\b\w/g, s => s.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function result_color(result) {
|
||||||
|
const result_color = {1: 'success', 2: 'danger', 3: 'dark'};
|
||||||
|
|
||||||
|
if (Number.isInteger(result)) return result_color[Number(result)];
|
||||||
|
else {
|
||||||
|
switch (result.toLowerCase()) {
|
||||||
|
case "negative":
|
||||||
|
return result_color[1];
|
||||||
|
break;
|
||||||
|
case "positive":
|
||||||
|
return result_color[2];
|
||||||
|
break;
|
||||||
|
default: // includes invalid
|
||||||
|
return result_color[3];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_and_show_pluslife_result(overall_result, channel_results){
|
||||||
|
$("#testresult").append("Pluslife says: " + result_text(overall_result));
|
||||||
|
|
||||||
|
channel_results_html = '';
|
||||||
|
for (const [channel, data] of Object.entries(channel_colors)) {
|
||||||
|
var result = result_text(channel_results[Number(channel)]);
|
||||||
|
var color = result_color(channel_results[Number(channel)]);
|
||||||
|
if (channel == "3"){
|
||||||
|
result = (result == "Positive") ? "Detected" : "Not Detected";
|
||||||
|
color = (result == "Detected") ? "primary" : color;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_results_html += `<span class="badge rounded-pill text-bg-${color} me-1">${channel}: ${result.slice(0,3)}</span>`;
|
||||||
|
}
|
||||||
|
$("#testresult_channels").append(channel_results_html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_chart(timestamp, overall_result, result_channels, sampledata){
|
||||||
|
$("#testdate").append(new Date(timestamp).toUTCString());
|
||||||
|
|
||||||
|
if (overall_result || result_channels)
|
||||||
|
parse_and_show_pluslife_result(overall_result, result_channels);
|
||||||
|
|
||||||
|
document.getElementById('datacontainer').hidden = false;
|
||||||
|
|
||||||
|
chart.data.labels.length = 0;
|
||||||
|
chart.data.datasets.forEach((dataset) => {
|
||||||
|
dataset.data.length = 0;
|
||||||
|
});
|
||||||
|
var offset = -1;
|
||||||
|
var data_index = 0;
|
||||||
|
var filled_array = Array(1000).fill(-1);
|
||||||
|
sampledata.forEach((sample) => {
|
||||||
|
var human_readable_time = get_human_readable_time(Math.floor(sample.samplingTime/10));
|
||||||
|
if (! chart.data.labels.includes(human_readable_time)){
|
||||||
|
chart.data.labels.push(human_readable_time);
|
||||||
|
offset += 1;
|
||||||
|
}
|
||||||
|
data_index = offset*7 + sample.startingChannel;
|
||||||
|
filled_array[data_index] = sample.firstChannelResult/64;
|
||||||
|
|
||||||
|
chart.data.datasets.forEach((dataset) => {
|
||||||
|
if (dataset.channel == sample.startingChannel)
|
||||||
|
dataset.data.push(sample.firstChannelResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function show_error(text) {
|
||||||
|
console.log(text);
|
||||||
|
$("#error").append(text);
|
||||||
|
$("#error").show();
|
||||||
|
}
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
if (data != null && typeof data == 'object') {
|
||||||
|
update_chart(Date.parse(json.test.data.temperatureSamples[0].time),
|
||||||
|
json.test.result.detectionResult,
|
||||||
|
json.test.result.channelResults,
|
||||||
|
json.test.data.samples);
|
||||||
|
} else {
|
||||||
|
show_error("Konnte Testdaten nicht laden");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
36
templates/base.html.twig
Normal file
36
templates/base.html.twig
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
|
||||||
|
<!-- <link rel="stylesheet" type="text/css" href="css/styles.css"> -->
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-dark sticky-top bg-primary shadow mb-4 justify-content-center">
|
||||||
|
<div class="container-fliud">
|
||||||
|
<a class="navbar-brand" href="index.php">PlusLifeWeb</a>
|
||||||
|
{% if block("header") is defined %}
|
||||||
|
<span class="navbar-text">{% block header %}{% endblock %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<div>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p/>
|
||||||
|
|
||||||
|
<script src="js/jquery.min.js"></script>
|
||||||
|
<script src="js/bootstrap.min.js"></script>
|
||||||
|
{% block js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
13
templates/error.html.twig
Normal file
13
templates/error.html.twig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}PlusLifeWeb - Fehler{% endblock %}
|
||||||
|
{% block header %}Fehler{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="bg-light p-3 rounded mt-3">
|
||||||
|
<pre class="mb-0">
|
||||||
|
{{ error }}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
23
templates/index.html.twig
Normal file
23
templates/index.html.twig
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}PlusLifeWeb{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if tests is not empty %}
|
||||||
|
<div>Test auswählen:</div>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
{% for test in tests %}
|
||||||
|
<tr><td><a href="test.php?id={{ test.id }}">{{ test.timestamp }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-light p-3 rounded mt-3">
|
||||||
|
<pre class="mb-0">Keine Tests gefunden.</pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
40
templates/test.html.twig
Normal file
40
templates/test.html.twig
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
{% block js %}
|
||||||
|
<script>
|
||||||
|
var json = {{ test|raw }};
|
||||||
|
</script>
|
||||||
|
<script src="js/chart.umd.js"></script>
|
||||||
|
<script src="js/pluslife.js"></script>
|
||||||
|
<script src="js/plweb.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}PlusLifeWeb Test Analyse{% endblock %}
|
||||||
|
{% block header %}Test Analyse{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="datacontainer" hidden="hidden">
|
||||||
|
<div class="row justify-content-center chart-container" style="position: relative; min-height: 400px; max-height:600px;">
|
||||||
|
<canvas id="data"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<strong>Datum: </strong><span id="testdate"></span><br />
|
||||||
|
<strong>Ergebnis: </strong><span id="testresult"></span><br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col justify-content-center" id="testresult_channels"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col">
|
||||||
|
<span id="error" style="color: red;display: none;"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
33
test.php
Normal file
33
test.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'Twig/autoload.php';
|
||||||
|
require_once 'config.php';
|
||||||
|
require_once 'functions.php';
|
||||||
|
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
|
$loader = new FilesystemLoader(__DIR__ . '/templates');
|
||||||
|
$twig = new Environment($loader);
|
||||||
|
|
||||||
|
// --------------
|
||||||
|
|
||||||
|
if (!isset($_GET['id']))
|
||||||
|
die(twig_error($twig, 400, "Keine ID angegeben."));
|
||||||
|
|
||||||
|
$id = (int)$_GET['id'];
|
||||||
|
|
||||||
|
$dbconn = pg_connect($dbconnstring)
|
||||||
|
or die(twig_error($twig, 500, 'Datenbankfehler: ' . pg_last_error()));
|
||||||
|
|
||||||
|
$query = "SELECT data FROM tests WHERE id=" . $id;
|
||||||
|
$result = pg_query($dbconn, $query)
|
||||||
|
or die(twig_error($twig, 'Datenbankfehler: ' . pg_last_error()));
|
||||||
|
|
||||||
|
$json = pg_fetch_result($result, 'data')
|
||||||
|
or die(twig_error($twig, 404, 'Test nicht gefunden.'));
|
||||||
|
|
||||||
|
pg_close($dbconn);
|
||||||
|
|
||||||
|
echo $twig->render('test.html.twig', [ 'test' => $json ]);
|
||||||
|
?>
|
||||||
|
|
BIN
vendor/bootstrap-5.3.7-dist.zip
vendored
Normal file
BIN
vendor/bootstrap-5.3.7-dist.zip
vendored
Normal file
Binary file not shown.
BIN
vendor/chart.js-4.5.0.tgz
vendored
Normal file
BIN
vendor/chart.js-4.5.0.tgz
vendored
Normal file
Binary file not shown.
2
vendor/jquery-3.7.1.min.js
vendored
Normal file
2
vendor/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
72
webhook.php
Normal file
72
webhook.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
include 'config.php';
|
||||||
|
include 'functions.php';
|
||||||
|
|
||||||
|
cors();
|
||||||
|
|
||||||
|
// Read the raw POST data
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true)
|
||||||
|
or die(return_error(400, 'Invalid data'));
|
||||||
|
|
||||||
|
switch ($data['event']) {
|
||||||
|
case "DEVICE_READY":
|
||||||
|
case "ALREADY_TESTING":
|
||||||
|
case "TEST_STARTED":
|
||||||
|
case "CONTINUE_TEST":
|
||||||
|
echo return_success();
|
||||||
|
send_notification($data['event']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "NEW_DATA":
|
||||||
|
echo return_success();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "TEST_FINISHED":
|
||||||
|
test_finished();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
die(return_error(400, 'Unknown event ' . $data['event']));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
function send_notification($msg, $url = null) {
|
||||||
|
global $hawebhook;
|
||||||
|
|
||||||
|
$post = [ 'message' => $msg ];
|
||||||
|
if ($url != null) $post['url'] = $url;
|
||||||
|
|
||||||
|
$ch = curl_init($hawebhook);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: application/json"));
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post));
|
||||||
|
$rc = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_finished() {
|
||||||
|
global $dbconnstring;
|
||||||
|
global $data;
|
||||||
|
global $test_url;
|
||||||
|
|
||||||
|
$dbconn = pg_connect($dbconnstring)
|
||||||
|
or die(return_error(500, 'connect Datenbankfehler: ' . pg_last_error()));
|
||||||
|
|
||||||
|
$query = "INSERT INTO tests VALUES (DEFAULT, $1, $2) RETURNING id";
|
||||||
|
$values = [ date('Y-m-d H:i:s'), json_encode($data) ];
|
||||||
|
|
||||||
|
$rc = pg_query_params($dbconn, $query, $values);
|
||||||
|
|
||||||
|
if ($rc) {
|
||||||
|
$id = pg_fetch_result($rc, 0, 0);
|
||||||
|
echo return_success();
|
||||||
|
send_notification("Test fertig. ID: " . $id, $test_url + $id);
|
||||||
|
} else {
|
||||||
|
echo return_error(500, 'insert Datenbankfehler: ' . pg_last_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
pg_close($dbconn);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
Loading…
x
Reference in New Issue
Block a user