parent
a021d25332
commit
937f825e82
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "itender",
|
"name": "itender",
|
||||||
"version": "1.0.2",
|
"version": "2.2.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "Tobias Hopp <tobi@gaminggeneration.de>",
|
"author": "Tobias Hopp <tobi@gaminggeneration.de>",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
|
@ -63,12 +63,12 @@
|
|||||||
|
|
||||||
|
|
||||||
.modalBlendIn {
|
.modalBlendIn {
|
||||||
animation: modalBlendIn 0.5s forwards;
|
animation: modalBlendIn 0.4s forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.modalBlendOut {
|
.modalBlendOut {
|
||||||
animation: modalBlendOut 0.8s forwards;
|
animation: modalBlendOut 0.4s forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,10 +30,10 @@ export class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static saveSettings() {
|
public static saveSettings() {
|
||||||
fs.writeFileSync(path.join(__dirname, "../config.json"), JSON.stringify(this._json));
|
fs.writeFileSync(path.join(__dirname, "../config.json"), JSON.stringify(this._json, null, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get(key: string): any {
|
public static get(key: "setupDone"|"secondsPer100ml"|"arduino_proxy_enabled"|"led_enabled"|"remote_enabled"|"hotspot_enabled"|"led_gpio"|"ambient_color"): any {
|
||||||
return this._json[key];
|
return this._json[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export class iTender {
|
|||||||
* How many seconds it takes to fill 100ml
|
* How many seconds it takes to fill 100ml
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
static secondsPer100ml: number = 35.3335;
|
static secondsPer100ml: number = 19.3335; // 35.3335
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sensitivity-Factor of the hx711 scales
|
* Sensitivity-Factor of the hx711 scales
|
||||||
|
17
src/main.ts
17
src/main.ts
@ -10,6 +10,7 @@ import Drink from "./database/Drink";
|
|||||||
import {MyGPIO} from "./MyGPIO";
|
import {MyGPIO} from "./MyGPIO";
|
||||||
import {ContainerHelper} from "./ContainerHelper";
|
import {ContainerHelper} from "./ContainerHelper";
|
||||||
import {Mixer} from "./Mixer";
|
import {Mixer} from "./Mixer";
|
||||||
|
import {ArduinoProxy} from "./ArduinoProxy";
|
||||||
|
|
||||||
const log = debug("itender:server");
|
const log = debug("itender:server");
|
||||||
|
|
||||||
@ -20,11 +21,25 @@ const wsApp = new WebsocketApp();
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
log("Starting...");
|
log("Starting...");
|
||||||
|
Settings.loadSettings();
|
||||||
|
|
||||||
|
|
||||||
await Database.connect();
|
await Database.connect();
|
||||||
|
if( Settings.get("arduino_proxy_enabled") as boolean )
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
await ArduinoProxy.connect();
|
||||||
|
} catch( e )
|
||||||
|
{
|
||||||
|
Settings.set("arduino_proxy_enabled",false);
|
||||||
|
Settings.setupDone = false;
|
||||||
|
log("Force iTender to setup, because proxy not connected!");
|
||||||
|
}
|
||||||
|
}
|
||||||
//await test();
|
//await test();
|
||||||
await app.listen();
|
await app.listen();
|
||||||
await wsApp.listen();
|
await wsApp.listen();
|
||||||
Settings.loadSettings();
|
|
||||||
|
|
||||||
iTender.setStatus(iTenderStatus.STARTING);
|
iTender.setStatus(iTenderStatus.STARTING);
|
||||||
await Utils.sleep(2000);
|
await Utils.sleep(2000);
|
||||||
|
@ -77,6 +77,17 @@ router.ws('/', async (ws, req, next) => {
|
|||||||
await container.save();
|
await container.save();
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let containers : IContainer[] = await Container.find();
|
||||||
|
for( let c of containers )
|
||||||
|
{
|
||||||
|
let find = data.find( (e) => {
|
||||||
|
return c._id == e.id;
|
||||||
|
} );
|
||||||
|
if( !find )
|
||||||
|
await Container.deleteOne({_id: c._id });
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +222,7 @@ router.ws('/', async (ws, req, next) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
log("Checkup failed");
|
log("Checkup failed");
|
||||||
content.success = false;
|
content.success = false;
|
||||||
content.msg = "Bei der Kommunikation mit dem Arduino Proxy ist ein Fehler aufgetreten.<br>Technische Details: " + e;
|
content.msg = "Bei der Kommunikation mit dem Arduino Proxy ist ein Fehler aufgetreten.<br><br><em>Technische Details: " + e + "</em>";
|
||||||
return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, content);
|
return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -288,13 +299,20 @@ router.ws('/', async (ws, req, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case RequestType.UPDATE: {
|
case RequestType.UPDATE: {
|
||||||
/*
|
if( !iTender.internetConnection )
|
||||||
- git pull
|
return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, false);
|
||||||
- yarn install
|
WebSocketHandler.answerRequest(msg.data["type"] as RequestType, true);
|
||||||
- yarn run compile
|
|
||||||
- (arduino update?)
|
|
||||||
- reboot
|
try {
|
||||||
*/
|
let result = await exec("/home/itender/itender/update.sh");
|
||||||
|
if( result.stderr )
|
||||||
|
await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der iTender konnte das Update nicht installieren.<br>Möglicherweise ist die Internetverbindung nicht ausreichend oder das Update enthält Fehler.<br>"));
|
||||||
|
} catch( e )
|
||||||
|
{
|
||||||
|
await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der iTender konnte das Update nicht installieren.<br>Möglicherweise ist die Internetverbindung nicht ausreichend oder das Update enthält Fehler.<br>"));
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +339,9 @@ router.ws('/', async (ws, req, next) => {
|
|||||||
"ip": ipAddr,
|
"ip": ipAddr,
|
||||||
"network": wifi.substring(wifi.indexOf('"')+1,wifi.length-2),
|
"network": wifi.substring(wifi.indexOf('"')+1,wifi.length-2),
|
||||||
"uptime": (await exec("uptime -p")).stdout.substring(3),
|
"uptime": (await exec("uptime -p")).stdout.substring(3),
|
||||||
"version": packageJson.version
|
"version": packageJson.version,
|
||||||
|
"author": "Tobias Hopp",
|
||||||
|
"contact": "tobi@gaminggeneration.de"
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, data);
|
return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, data);
|
||||||
|
@ -82,7 +82,7 @@ export class Containers {
|
|||||||
let selectIngredient = document.createElement("select");
|
let selectIngredient = document.createElement("select");
|
||||||
selectIngredient.classList.add("hidden");
|
selectIngredient.classList.add("hidden");
|
||||||
selectIngredient.classList.add("input");
|
selectIngredient.classList.add("input");
|
||||||
selectIngredient.style.width = "50%"
|
selectIngredient.style.width = "35%"
|
||||||
|
|
||||||
// When ingredient is changed
|
// When ingredient is changed
|
||||||
selectIngredient.onchange = () => {
|
selectIngredient.onchange = () => {
|
||||||
@ -189,6 +189,7 @@ export class Containers {
|
|||||||
selectContainer.dispatchEvent(event);
|
selectContainer.dispatchEvent(event);
|
||||||
selectIngredient.dispatchEvent(event);
|
selectIngredient.dispatchEvent(event);
|
||||||
});
|
});
|
||||||
|
modal.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
modal.open();
|
modal.open();
|
||||||
|
@ -177,7 +177,7 @@ export class Modal {
|
|||||||
modalContent.classList.remove("modalBlendOut");
|
modalContent.classList.remove("modalBlendOut");
|
||||||
modal.classList.remove("modalBlendOut");
|
modal.classList.remove("modalBlendOut");
|
||||||
this.modalInClose = false;
|
this.modalInClose = false;
|
||||||
}, 800);
|
}, 402);
|
||||||
|
|
||||||
this.currentModalId = undefined;
|
this.currentModalId = undefined;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,9 @@ export class Settings {
|
|||||||
|
|
||||||
const reload = document.getElementById("settings_reload") as HTMLButtonElement;
|
const reload = document.getElementById("settings_reload") as HTMLButtonElement;
|
||||||
reload.onclick = () => window.location.reload();
|
reload.onclick = () => window.location.reload();
|
||||||
|
|
||||||
|
const update = document.getElementById("settings_update") as HTMLButtonElement;
|
||||||
|
update.onclick = () => this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static onClickRefreshDrinks() {
|
private static onClickRefreshDrinks() {
|
||||||
@ -41,21 +44,17 @@ export class Settings {
|
|||||||
|
|
||||||
th.append(tdTh1, tdTh2);
|
th.append(tdTh1, tdTh2);
|
||||||
|
|
||||||
let x = [["internet","Internet-Konnektivität"], ["ip","IP-Adresse"], ["network","WiFi-Netzwerk"], ["uptime","Gerät aktiv seit"], ["version", "Version"]];
|
let x = [["internet", "Internet-Konnektivität"], ["ip", "IP-Adresse"], ["network", "WiFi-Netzwerk"], ["uptime", "Gerät aktiv seit"], ["version", "Version"], ["author", "Entwickler"], ["contact", "Support-Adresse"]];
|
||||||
for( let y of x )
|
for (let y of x) {
|
||||||
{
|
|
||||||
let tr = document.createElement("tr");
|
let tr = document.createElement("tr");
|
||||||
let td1 = document.createElement("td");
|
let td1 = document.createElement("td");
|
||||||
let td2 = document.createElement("td");
|
let td2 = document.createElement("td");
|
||||||
|
|
||||||
td1.innerText = y[1];
|
td1.innerText = y[1];
|
||||||
td1.style.fontWeight = "bold";
|
td1.style.fontWeight = "bold";
|
||||||
if( payload.data[y[0]] === true || payload.data[y[0]] === false )
|
if (payload.data[y[0]] === true || payload.data[y[0]] === false) {
|
||||||
{
|
|
||||||
td2.innerText = payload.data[y[0]] == true ? "Verbunden" : "Getrennt";
|
td2.innerText = payload.data[y[0]] == true ? "Verbunden" : "Getrennt";
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
td2.innerText = payload.data[y[0]];
|
td2.innerText = payload.data[y[0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,4 +69,23 @@ export class Settings {
|
|||||||
modal.open();
|
modal.open();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static update() {
|
||||||
|
WebWebSocketHandler.request(RequestType.UPDATE, null).then((payload) => {
|
||||||
|
let modal = new Modal("info", "System-Update");
|
||||||
|
let txt = document.createElement("p");
|
||||||
|
if (payload.data as boolean) {
|
||||||
|
|
||||||
|
txt.innerHTML = `Der iTender wird nun aktualisiert!<br><br>
|
||||||
|
Sobald das Update installiert ist, wird das System neu gestartet.<br>Die dadurch hergehende Verbindungswarnung kann ignoriert werden.<br>Der iTender stellt die Verbindung automatisch wieder her.<br><br><span style="color:red;font-weight: bold">Schalten Sie das System nicht aus und entfernen Sie nicht das Netzkabel!</span>`;
|
||||||
|
modal.addContent(txt);
|
||||||
|
modal.loader = true;
|
||||||
|
} else {
|
||||||
|
txt.innerHTML = `Das System kann nicht aktualisiert werden.<br>iTender hat keine Internet-Konnektivität fest gestellt.<br>Versuchen Sie es zu einem späteren Zeitpunkt erneut.`;
|
||||||
|
modal.addButton(ButtonType.PRIMARY, "Schließen", () => modal.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.open();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
@ -199,9 +199,9 @@ Dort werden die Behälter definiert, welche in den iTender gestellt werden.<br>D
|
|||||||
|
|
||||||
// Check
|
// Check
|
||||||
let answer = await WebWebSocketHandler.request(RequestType.CHECK, newConf);
|
let answer = await WebWebSocketHandler.request(RequestType.CHECK, newConf);
|
||||||
console.log(answer);
|
|
||||||
if (!(answer.data["success"] as boolean)) {
|
if (!(answer.data["success"] as boolean)) {
|
||||||
ele.innerHTML = `Die Konfiguration weist Fehler auf!<br>${answer.data["msg"]}`;
|
ele.innerHTML = `Die Überprüfung schlug fehl!<br>${answer.data["msg"]}`;
|
||||||
await errorModal.open();
|
await errorModal.open();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -552,8 +552,11 @@ Mindestens ein Sensor konnte nicht kalibriert werden.<br>${data.msg}<br>`;
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
c.classList.remove("error");
|
c.classList.remove("error");
|
||||||
c2.classList.remove("error");
|
c2.classList.remove("error");
|
||||||
|
setTimeout( () => {
|
||||||
sel.classList.remove("error");
|
sel.classList.remove("error");
|
||||||
sel2.classList.remove("error");
|
sel2.classList.remove("error");
|
||||||
|
}, 1500 );
|
||||||
|
|
||||||
}, 2200);
|
}, 2200);
|
||||||
returner = false;
|
returner = false;
|
||||||
}
|
}
|
||||||
|
@ -42,8 +42,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
let container: IContainer;
|
let container: IContainer;
|
||||||
let bottomContainers = document.getElementById("menuContainers") as HTMLDivElement;
|
let bottomContainers = document.getElementById("menuContainers") as HTMLDivElement;
|
||||||
bottomContainers.innerHTML = "";
|
bottomContainers.innerHTML = "";
|
||||||
for( container of payload.data )
|
for (container of payload.data) {
|
||||||
{
|
|
||||||
let containerDiv = document.createElement("div") as HTMLDivElement;
|
let containerDiv = document.createElement("div") as HTMLDivElement;
|
||||||
containerDiv.classList.add("container");
|
containerDiv.classList.add("container");
|
||||||
|
|
||||||
@ -59,9 +58,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
if (pcnt < 5)
|
if (pcnt < 5)
|
||||||
containerDiv.style.backgroundColor = "red";
|
containerDiv.style.backgroundColor = "red";
|
||||||
else if (pcnt < 15)
|
else if (pcnt < 15)
|
||||||
containerDiv.style.backgroundColor = "#ef4f00";
|
containerDiv.style.backgroundColor = "#EF4F00";
|
||||||
else if (pcnt < 30)
|
else if (pcnt < 30)
|
||||||
containerDiv.style.backgroundColor = "#ff5400";
|
containerDiv.style.backgroundColor = "#FF5400";
|
||||||
|
|
||||||
containerDiv.append(span);
|
containerDiv.append(span);
|
||||||
|
|
||||||
@ -75,18 +74,6 @@ function setupOnClickEvents() {
|
|||||||
const menuBtn = document.getElementById("menuBtn") as HTMLButtonElement;
|
const menuBtn = document.getElementById("menuBtn") as HTMLButtonElement;
|
||||||
menuBtn.disabled = true;
|
menuBtn.disabled = true;
|
||||||
|
|
||||||
let timer = 0;
|
|
||||||
function mouseDown() {
|
|
||||||
timer = Date.now();
|
|
||||||
}
|
|
||||||
function mouseUp() {
|
|
||||||
if( ( Date.now() - timer ) / 1000 > 5 )
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
menuBtn.onmousedown = mouseDown;
|
|
||||||
menuBtn.ontouchstart = mouseDown;
|
|
||||||
menuBtn.onmouseup = mouseUp;
|
|
||||||
menuBtn.ontouchend = mouseUp;
|
|
||||||
|
|
||||||
function doMenu() {
|
function doMenu() {
|
||||||
if (WebHandler.currentPane != Pane.MENU) {
|
if (WebHandler.currentPane != Pane.MENU) {
|
||||||
@ -107,43 +94,43 @@ function setupOnClickEvents() {
|
|||||||
|
|
||||||
menuStatsBtn.onclick = async () => {
|
menuStatsBtn.onclick = async () => {
|
||||||
|
|
||||||
let statsModal = new Modal("stats", "Statistiken");
|
let modal = new Modal("stats", "Statistiken");
|
||||||
|
|
||||||
let txt = document.createElement("p");
|
let table = document.createElement("table");
|
||||||
txt.innerHTML = `Folgende Statistiken wurden erfasst.`;
|
table.style.marginLeft = "auto";
|
||||||
statsModal.addContent(txt);
|
table.style.marginRight = "auto";
|
||||||
|
|
||||||
let div = document.createElement("div");
|
let th = document.createElement("th");
|
||||||
div.style.textAlign = "left";
|
table.append(th);
|
||||||
statsModal.addContent(div);
|
|
||||||
|
|
||||||
let list = document.createElement("ul");
|
let tdTh1 = document.createElement("td");
|
||||||
div.append(list);
|
tdTh1.innerText = "";
|
||||||
|
let tdTh2 = document.createElement("td");
|
||||||
|
tdTh2.innerText = "";
|
||||||
|
|
||||||
statsModal.addContent(document.createElement("br"));
|
th.append(tdTh1, tdTh2);
|
||||||
statsModal.addButton(ButtonType.PRIMARY, "Schließen", () => statsModal.close());
|
|
||||||
|
|
||||||
WebWebSocketHandler.request(RequestType.STATS).then((payload) => {
|
WebWebSocketHandler.request(RequestType.STATS).then((payload) => {
|
||||||
let li = document.createElement("li");
|
let x = [["drinks_finished", "Ausgegebene Cocktails"], ["drink_most", "Beliebtester Cocktail"], ["count_cocktails", "Anzahl an Cocktails"], ["count_ingredients", "Anzahl an Zutaten"]];
|
||||||
console.log(payload);
|
for (let y of x) {
|
||||||
li.innerText = "Cocktails ausgegeben: " + payload.data["drinks_finished"];
|
let tr = document.createElement("tr");
|
||||||
list.append(li);
|
let td1 = document.createElement("td");
|
||||||
|
let td2 = document.createElement("td");
|
||||||
|
|
||||||
li = document.createElement("li");
|
td1.innerText = y[1];
|
||||||
li.innerText = "Häufigster Cocktail: " + payload.data["drink_most"];
|
td1.style.fontWeight = "bold";
|
||||||
list.append(li);
|
td2.innerText = payload.data[y[0]];
|
||||||
|
tr.append(td1, td2);
|
||||||
|
|
||||||
li = document.createElement("li");
|
table.append(tr);
|
||||||
li.innerText = "Anzahl Ingredients: " + payload.data["count_ingredients"];
|
}
|
||||||
list.append(li);
|
modal.addContent(table);
|
||||||
|
|
||||||
li = document.createElement("li");
|
|
||||||
li.innerText = "Anzahl Cocktails: " + payload.data["count_cocktails"];
|
|
||||||
list.append(li);
|
|
||||||
|
|
||||||
|
modal.addContent(document.createElement("br"));
|
||||||
|
modal.addButton(ButtonType.PRIMARY, "Schließen", () => modal.close());
|
||||||
|
modal.open();
|
||||||
});
|
});
|
||||||
|
|
||||||
await statsModal.open();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuSettingsBtn = document.getElementById("menu_settings") as HTMLButtonElement;
|
const menuSettingsBtn = document.getElementById("menu_settings") as HTMLButtonElement;
|
||||||
@ -159,8 +146,6 @@ function setupOnClickEvents() {
|
|||||||
Settings.addListeners();
|
Settings.addListeners();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let wsHandler;
|
let wsHandler;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
cd /home/itender/itender || exit -1
|
||||||
git pull "https://tobiash:!IwedwrimmVeudiweN!@git.gaminggeneration.de/tobiash/itender.git" --quiet
|
git pull "https://tobiash:!IwedwrimmVeudiweN!@git.gaminggeneration.de/tobiash/itender.git" --quiet
|
||||||
yarn
|
yarn
|
||||||
yarn run compile
|
yarn run compile
|
||||||
sudo systemctl restart itender
|
sudo systemctl restart itender
|
||||||
|
exit 0
|
@ -11,7 +11,7 @@ block setup
|
|||||||
input#ledGPIO.input(type="number" value="40" style="width:15%" disabled="disabled")
|
input#ledGPIO.input(type="number" value="40" style="width:15%" disabled="disabled")
|
||||||
div.inputGroup
|
div.inputGroup
|
||||||
label Ambiente Farbe
|
label Ambiente Farbe
|
||||||
input#ambientColor.input(type="color" value="#05445E" style="width:15%" disabled="disabled")
|
input#ambientColor.input(type="color" value="#05445E" style="width:15%")
|
||||||
|
|
||||||
div#setupExtraDiv
|
div#setupExtraDiv
|
||||||
h1 Erweiterte Einstellungen
|
h1 Erweiterte Einstellungen
|
||||||
@ -54,11 +54,13 @@ block menu
|
|||||||
|
|
||||||
block settings
|
block settings
|
||||||
// Settings
|
// Settings
|
||||||
button.btn.btn-primary#settings_refreshDrinks Getränke herunterladen
|
button.btn.btn-primary#settings_refreshDrinks Getränke aktualisieren
|
||||||
button.btn.btn-primary#settings_update System aktualisieren
|
button.btn.btn-primary#settings_deleteDrinks(disabled="disabled") Getränke-DB löschen
|
||||||
button.btn.btn-primary#settings_getInfo Systeminformationen
|
|
||||||
button.btn.btn-primary#settings_reload Oberfläche neu starten
|
button.btn.btn-primary#settings_reload Oberfläche neu starten
|
||||||
|
button.btn.btn-primary#settings_getInfo Systeminformationen
|
||||||
|
button.btn.btn-primary#settings_update System aktualisieren
|
||||||
|
button.btn.btn-primary#settings_restart(disabled="disabled") iTender neu starten
|
||||||
|
button.btn.btn-primary#settings_shutdown(disabled="disabled") iTender herunterfahren
|
||||||
|
|
||||||
|
|
||||||
block main
|
block main
|
||||||
|
Loading…
x
Reference in New Issue
Block a user