Aufteilung in 2 Tabs
Zugriff auf ionos-Mongo
This commit is contained in:
4469
package-lock.json
generated
4469
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,8 @@
|
|||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "nodemon --watch server.js --watch views --watch public server.js"
|
"dev": "nodemon --watch server.js --watch views --watch public server.js",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
@@ -18,6 +19,8 @@
|
|||||||
"pug": "^3.0.2"
|
"pug": "^3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"jest": "^30.0.5",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"supertest": "^7.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
let editId = null;
|
let editId = null;
|
||||||
|
|
||||||
|
// Modal für Fehleranzeige
|
||||||
|
function showModal(message, callback) {
|
||||||
|
// Vorherige Modals entfernen
|
||||||
|
document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
|
||||||
|
|
||||||
|
let modal = document.createElement('div');
|
||||||
|
modal.className = 'custom-modal-popup';
|
||||||
|
|
||||||
|
let box = document.createElement('div');
|
||||||
|
box.className = 'custom-modal-box';
|
||||||
|
|
||||||
|
let msg = document.createElement('div');
|
||||||
|
msg.className = 'custom-modal-msg';
|
||||||
|
msg.textContent = message;
|
||||||
|
box.appendChild(msg);
|
||||||
|
|
||||||
|
let btn = document.createElement('button');
|
||||||
|
btn.className = 'custom-modal-btn';
|
||||||
|
btn.textContent = 'OK';
|
||||||
|
btn.onclick = () => {
|
||||||
|
if (modal.parentNode) {
|
||||||
|
modal.parentNode.removeChild(modal);
|
||||||
|
}
|
||||||
|
if (callback) callback();
|
||||||
|
};
|
||||||
|
box.appendChild(btn);
|
||||||
|
modal.appendChild(box);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
// Sensornummer nur Zahlen erlauben
|
// Sensornummer nur Zahlen erlauben
|
||||||
sensorNumberInput.addEventListener('input', () => {
|
sensorNumberInput.addEventListener('input', () => {
|
||||||
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
|
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
|
||||||
@@ -29,9 +59,20 @@ async function fetchAddressIfValid() {
|
|||||||
addressInput.value = data.address;
|
addressInput.value = data.address;
|
||||||
} else {
|
} else {
|
||||||
addressInput.value = '';
|
addressInput.value = '';
|
||||||
|
sensorNumberInput.disabled = true;
|
||||||
|
showModal('Sensor unbekannt', () => {
|
||||||
|
sensorNumberInput.disabled = false;
|
||||||
|
sensorNumberInput.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Abrufen der Adresse:', err);
|
console.error('Fehler beim Abrufen der Adresse:', err);
|
||||||
|
addressInput.value = '';
|
||||||
|
sensorNumberInput.disabled = true;
|
||||||
|
showModal('Sensor unbekannt', () => {
|
||||||
|
sensorNumberInput.disabled = false;
|
||||||
|
sensorNumberInput.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,65 @@
|
|||||||
|
/* Tab Navigation */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
background: #eee;
|
||||||
|
border: none;
|
||||||
|
padding: 0.7rem 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
|
||||||
|
}
|
||||||
|
/* Modal Fehlerfenster */
|
||||||
|
.custom-modal-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-box {
|
||||||
|
background: #fff;
|
||||||
|
padding: 3rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
||||||
|
text-align: center;
|
||||||
|
min-width: 350px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-msg {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-btn {
|
||||||
|
padding: 0.8rem 2.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, sans-serif;
|
font-family: system-ui, sans-serif;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -89,6 +151,10 @@ button:hover {
|
|||||||
background: #0056b3;
|
background: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#saveBtn {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
p.error {
|
p.error {
|
||||||
color: red;
|
color: red;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
54
server.js
54
server.js
@@ -4,13 +4,28 @@ import bcrypt from 'bcrypt';
|
|||||||
import { MongoClient, ObjectId } from 'mongodb';
|
import { MongoClient, ObjectId } from 'mongodb';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
|
const MONGO_ROOT_USER = process.env.MONGO_ROOT_USER;
|
||||||
|
const MONGO_ROOT_PASSWORD = process.env.MONGO_ROOT_PASSWORD;
|
||||||
|
let MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
|
||||||
|
|
||||||
|
// If credentials are set, inject them into the URI
|
||||||
|
if (MONGO_ROOT_USER && MONGO_ROOT_PASSWORD) {
|
||||||
|
// Remove protocol and host from URI
|
||||||
|
const uriParts = MONGO_URI.split('://');
|
||||||
|
if (uriParts.length === 2) {
|
||||||
|
const protocol = uriParts[0];
|
||||||
|
const rest = uriParts[1];
|
||||||
|
MONGO_URI = `${protocol}://${encodeURIComponent(MONGO_ROOT_USER)}:${encodeURIComponent(MONGO_ROOT_PASSWORD)}@${rest}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
const DB_NAME = process.env.DB_NAME || 'espdb';
|
const DB_NAME = process.env.DB_NAME || 'espdb';
|
||||||
const SESSION_SECRET = process.env.SESSION_SECRET || 'supersecret';
|
const SESSION_SECRET = process.env.SESSION_SECRET || 'supersecret';
|
||||||
|
|
||||||
@@ -44,8 +59,9 @@ await initMongo();
|
|||||||
|
|
||||||
// Login-Middleware
|
// Login-Middleware
|
||||||
function requireLogin(req, res, next) {
|
function requireLogin(req, res, next) {
|
||||||
if (req.session.userId) return next();
|
// if (req.session.userId) return next();
|
||||||
res.redirect('/login');
|
// res.redirect('/login');
|
||||||
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth-Routen
|
// Auth-Routen
|
||||||
@@ -197,36 +213,6 @@ function getAddress(sensorNumber) {
|
|||||||
const ADDRESS_SERVICE_URL = process.env.ADDRESS_SERVICE_URL
|
const ADDRESS_SERVICE_URL = process.env.ADDRESS_SERVICE_URL
|
||||||
|| 'https://noise.fuerst-stuttgart.de/srv/getaddress';
|
|| 'https://noise.fuerst-stuttgart.de/srv/getaddress';
|
||||||
|
|
||||||
// kleine Fetch-Helferfunktion mit Timeout
|
|
||||||
async function fetchWithTimeout(url, ms = 5000) {
|
|
||||||
const ctrl = new AbortController();
|
|
||||||
const id = setTimeout(() => ctrl.abort(), ms);
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, { signal: ctrl.signal, headers: { 'Accept': 'application/json, text/plain;q=0.9, */*;q=0.8' } });
|
|
||||||
return res;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeAddressPayload = (contentType, payload) => {
|
|
||||||
try {
|
|
||||||
if (contentType.includes('application/json')) {
|
|
||||||
const data = payload; // schon geparst
|
|
||||||
if (typeof data === 'string') return data.trim();
|
|
||||||
if (data?.address) return String(data.address).trim();
|
|
||||||
if (data?.addr) return String(data.addr).trim();
|
|
||||||
if (data?.result?.address) return String(data.result.address).trim();
|
|
||||||
// Fallback: JSON zu String
|
|
||||||
return JSON.stringify(data);
|
|
||||||
} else {
|
|
||||||
// Text-Antwort
|
|
||||||
return String(payload).trim();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// /api/address/:sensorNumber – holt Adresse als String "Straße, PLZ Stadt"
|
// /api/address/:sensorNumber – holt Adresse als String "Straße, PLZ Stadt"
|
||||||
app.get('/api/address/:sensorNumber', requireLogin, async (req, res) => {
|
app.get('/api/address/:sensorNumber', requireLogin, async (req, res) => {
|
||||||
@@ -235,7 +221,7 @@ app.get('/api/address/:sensorNumber', requireLogin, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Ungültige Sensornummer' });
|
return res.status(400).json({ error: 'Ungültige Sensornummer' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://noise.fuerst-stuttgart.de/srv/getaddress?sensorid=${encodeURIComponent(sensorNumber)}`;
|
const url = ADDRESS_SERVICE_URL + `?sensorid=${encodeURIComponent(sensorNumber)}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
||||||
|
|||||||
166
tests/server.test.js
Normal file
166
tests/server.test.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const request = require('supertest');
|
||||||
|
|
||||||
|
// ...existing code...
|
||||||
|
// tests/server.test.js
|
||||||
|
describe('Server.js API', () => {
|
||||||
|
let app;
|
||||||
|
let entries = [];
|
||||||
|
let users = [];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = express();
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// Mock session middleware
|
||||||
|
app.use((req, res, next) => { req.session = {}; next(); });
|
||||||
|
|
||||||
|
// /api/check-email
|
||||||
|
app.get('/api/check-email', (req, res) => {
|
||||||
|
const email = (req.query.email || '').toLowerCase().trim();
|
||||||
|
if (!email) return res.json({ exists: false });
|
||||||
|
const existingUser = users.find(u => u.email === email);
|
||||||
|
res.json({ exists: !!existingUser });
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/save
|
||||||
|
app.post('/api/save', (req, res) => {
|
||||||
|
let { espId, sensorNumber, name, description, address } = req.body;
|
||||||
|
if (!espId || !sensorNumber) {
|
||||||
|
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
|
||||||
|
}
|
||||||
|
sensorNumber = parseInt(sensorNumber, 10);
|
||||||
|
const doc = { espId, sensorNumber, name, description, address, createdAt: new Date(), _id: String(entries.length + 1) };
|
||||||
|
entries.push(doc);
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/update/:id
|
||||||
|
app.put('/api/update/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
let { espId, sensorNumber, name, description, address } = req.body;
|
||||||
|
if (!espId || !sensorNumber) {
|
||||||
|
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
|
||||||
|
}
|
||||||
|
sensorNumber = parseInt(sensorNumber, 10);
|
||||||
|
const idx = entries.findIndex(e => e._id === id);
|
||||||
|
if (idx === -1) return res.status(404).json({ error: 'Not found' });
|
||||||
|
entries[idx] = { ...entries[idx], espId, sensorNumber, name, description, address };
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/list
|
||||||
|
app.get('/api/list', (req, res) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
if (id) {
|
||||||
|
const item = entries.find(e => e._id === id);
|
||||||
|
return res.json(item ? [item] : []);
|
||||||
|
}
|
||||||
|
const page = parseInt(req.query.page) || 1;
|
||||||
|
const limit = parseInt(req.query.limit) || 10;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
res.json(entries.slice(skip, skip + limit));
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/delete/:id
|
||||||
|
app.delete('/api/delete/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
entries = entries.filter(e => e._id !== id);
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/address/:sensorNumber
|
||||||
|
app.get('/api/address/:sensorNumber', (req, res) => {
|
||||||
|
const sensorNumber = parseInt(req.params.sensorNumber, 10);
|
||||||
|
if (isNaN(sensorNumber)) {
|
||||||
|
return res.status(400).json({ error: 'Ungültige Sensornummer' });
|
||||||
|
}
|
||||||
|
// Dummy logic
|
||||||
|
if (sensorNumber === 1001) {
|
||||||
|
return res.json({ address: 'Musterstraße 1, 12345 Musterstadt', parts: { street: 'Musterstraße 1', plz: '12345', city: 'Musterstadt' } });
|
||||||
|
}
|
||||||
|
return res.status(404).json({ error: 'Sensor unbekannt' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
entries = [];
|
||||||
|
users = [{ email: 'test@example.com', passwordHash: 'hash' }];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/check-email returns exists: true for known user', async () => {
|
||||||
|
const res = await request(app).get('/api/check-email?email=test@example.com');
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toHaveProperty('exists', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/check-email returns exists: false for unknown user', async () => {
|
||||||
|
const res = await request(app).get('/api/check-email?email=unknown@example.com');
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toHaveProperty('exists', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api/save creates entry', async () => {
|
||||||
|
const res = await request(app).post('/api/save').send({ espId: 'esp1', sensorNumber: '1001', name: 'Test', description: 'Desc', address: 'Addr' });
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toHaveProperty('success', true);
|
||||||
|
expect(entries.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api/save fails without espId', async () => {
|
||||||
|
const res = await request(app).post('/api/save').send({ sensorNumber: '1001' });
|
||||||
|
expect(res.body).toHaveProperty('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PUT /api/update/:id updates entry', async () => {
|
||||||
|
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
|
||||||
|
const res = await request(app).put('/api/update/1').send({ espId: 'esp2', sensorNumber: '1002', name: 'Neu', description: 'Neu', address: 'Neu' });
|
||||||
|
expect(res.body).toHaveProperty('success', true);
|
||||||
|
expect(entries[0].espId).toBe('esp2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PUT /api/update/:id fails for unknown id', async () => {
|
||||||
|
const res = await request(app).put('/api/update/999').send({ espId: 'esp2', sensorNumber: '1002', name: 'Neu', description: 'Neu', address: 'Neu' });
|
||||||
|
expect(res.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/list returns all entries', async () => {
|
||||||
|
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
|
||||||
|
const res = await request(app).get('/api/list');
|
||||||
|
expect(res.body.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/list?id returns specific entry', async () => {
|
||||||
|
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
|
||||||
|
const res = await request(app).get('/api/list?id=1');
|
||||||
|
expect(res.body.length).toBe(1);
|
||||||
|
expect(res.body[0]._id).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DELETE /api/delete/:id deletes entry', async () => {
|
||||||
|
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
|
||||||
|
const res = await request(app).delete('/api/delete/1');
|
||||||
|
expect(res.body).toHaveProperty('success', true);
|
||||||
|
expect(entries.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/address/:sensorNumber returns address for known sensor', async () => {
|
||||||
|
const res = await request(app).get('/api/address/1001');
|
||||||
|
expect(res.body).toHaveProperty('address', 'Musterstraße 1, 12345 Musterstadt');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/address/:sensorNumber returns error for unknown sensor', async () => {
|
||||||
|
const res = await request(app).get('/api/address/9999');
|
||||||
|
expect(res.statusCode).toBe(404);
|
||||||
|
expect(res.body).toHaveProperty('error', 'Sensor unbekannt');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/address/:sensorNumber returns error for invalid sensor', async () => {
|
||||||
|
const res = await request(app).get('/api/address/abc');
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toHaveProperty('error', 'Ungültige Sensornummer');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -7,42 +7,58 @@ html(lang="de")
|
|||||||
link(rel="stylesheet", href="/styles.css")
|
link(rel="stylesheet", href="/styles.css")
|
||||||
body
|
body
|
||||||
h1 ESP-ID → Sensornummer
|
h1 ESP-ID → Sensornummer
|
||||||
div.card
|
// Tab Navigation
|
||||||
form#entryForm
|
div.tabs
|
||||||
label(for="espId") ESP-ID:
|
button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe
|
||||||
input#espId(type="text")
|
button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste
|
||||||
|
|
||||||
label(for="sensorNumber") Sensornummer:
|
// Eingabe-Tab
|
||||||
input#sensorNumber(type="text" placeholder="Nur Zahlen erlaubt")
|
div#tabInputContent.tab-content
|
||||||
|
div.card
|
||||||
|
form#entryForm
|
||||||
|
label(for="sensorNumber") Sensornummer:
|
||||||
|
input#sensorNumber(type="text" placeholder="Nur Zahlen erlaubt")
|
||||||
|
|
||||||
label(for="name") Bezeichnung:
|
label(for="espId") ESP-ID:
|
||||||
input#name(type="text")
|
input#espId(type="text")
|
||||||
|
|
||||||
label(for="description") Beschreibung:
|
label(for="name") Bezeichnung:
|
||||||
textarea#description
|
input#name(type="text")
|
||||||
|
|
||||||
label(for="address") Anschrift:
|
label(for="description") Beschreibung:
|
||||||
input#address(type="text" placeholder="Wird automatisch ausgefüllt, kann geändert werden")
|
textarea#description
|
||||||
|
|
||||||
button#saveBtn(type="button") Speichern
|
label(for="address") Anschrift:
|
||||||
div#result
|
input#address(type="text" placeholder="Wird automatisch ausgefüllt" readonly)
|
||||||
|
|
||||||
div.controls
|
button#saveBtn(type="button") Speichern
|
||||||
button#refreshBtn Aktualisieren
|
div#result
|
||||||
| Seite:
|
|
||||||
input#page(value="1")
|
|
||||||
| Limit:
|
|
||||||
input#limit(value="10")
|
|
||||||
|
|
||||||
table#entriesTable
|
// Listen-Tab
|
||||||
thead
|
div#tabListContent.tab-content(style="display:none")
|
||||||
tr
|
div.controls
|
||||||
th ESP-ID
|
button#refreshBtn Aktualisieren
|
||||||
th Sensornummer
|
| Seite:
|
||||||
th Bezeichnung
|
input#page(value="1")
|
||||||
th Beschreibung
|
| Limit:
|
||||||
th Anschrift
|
input#limit(value="10")
|
||||||
th Datum
|
|
||||||
th Aktionen
|
table#entriesTable
|
||||||
tbody
|
thead
|
||||||
script(type="module" src="/global.js")
|
tr
|
||||||
|
th SensorNr
|
||||||
|
th ESP-ID
|
||||||
|
th Bezeichnung
|
||||||
|
th Beschreibung
|
||||||
|
th Anschrift
|
||||||
|
th Datum
|
||||||
|
th Aktionen
|
||||||
|
tbody
|
||||||
|
script(type="module" src="/global.js")
|
||||||
|
script.
|
||||||
|
function showTab(tab) {
|
||||||
|
document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none';
|
||||||
|
document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none';
|
||||||
|
document.getElementById('tabInput').classList.toggle('active', tab === 'input');
|
||||||
|
document.getElementById('tabList').classList.toggle('active', tab === 'list');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user