Compare commits
10 Commits
81c92dcd5c
...
633e1ea4f3
| Author | SHA1 | Date | |
|---|---|---|---|
| 633e1ea4f3 | |||
| 5ccd37b931 | |||
| 6466bb2d92 | |||
| 90662d8d0b | |||
| 6c0aaf0123 | |||
| 326b30206d | |||
| 4a47d3cd86 | |||
| e7e6cb97ca | |||
| b3b411db1a | |||
| 61f8dec32f |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.env
|
||||
.env
|
||||
*.pdf
|
||||
|
||||
63
build_and_copy.sh
Executable file
63
build_and_copy.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Build Docker-Container
|
||||
#
|
||||
# Call: buildit.sh name [target]
|
||||
#
|
||||
# The Dockerfile must be named like Dockerfile_name
|
||||
#
|
||||
# 2018-09-20 rxf
|
||||
# - before sending docker image to remote, tag actual remote image
|
||||
#
|
||||
# 2018-09-14 rxf
|
||||
# - first Version
|
||||
#
|
||||
|
||||
set -x
|
||||
port=""
|
||||
orgName=esp2sensor
|
||||
name=esp2sensor
|
||||
|
||||
usage()
|
||||
{
|
||||
echo "Usage build_and_copy.sh [-p port] [-n name] target"
|
||||
echo " Build docker container $name and copy to target"
|
||||
echo "Params:"
|
||||
echo " target: Where to copy the container to "
|
||||
echo " -p port: ssh port (default 22)"
|
||||
echo " -n name: new name for container (default: $orgName)"
|
||||
}
|
||||
|
||||
while getopts n:p:h? o
|
||||
do
|
||||
case "$o" in
|
||||
n) name="$OPTARG";;
|
||||
p) port="-p $OPTARG";;
|
||||
h) usage; exit 0;;
|
||||
*) usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
if [[ -z "$target" ]]; then
|
||||
target=$1
|
||||
shift
|
||||
else
|
||||
echo "bad option $1"
|
||||
# exit 1
|
||||
shift
|
||||
fi
|
||||
done
|
||||
|
||||
docker build -f Dockerfile_$orgName --no-cache -t $name .
|
||||
|
||||
dat=`date +%Y%m%d%H%M`
|
||||
|
||||
if [ "$target" == "localhost" ]
|
||||
then
|
||||
docker tag $name $name:V_$dat
|
||||
exit
|
||||
fi
|
||||
|
||||
ssh $port $target "docker tag $name $name:V_$dat"
|
||||
docker save $name | bzip2 | pv | ssh $port $target 'bunzip2 | docker load'
|
||||
66
db/mongo.js
66
db/mongo.js
@@ -1,30 +1,60 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
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';
|
||||
dotenv.config();
|
||||
|
||||
if (MONGO_ROOT_USER && MONGO_ROOT_PASSWORD) {
|
||||
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 MONGOHOST = process.env.MONGOHOST || 'localhost'
|
||||
const MONGOPORT = process.env.MONGOPORT || 27017
|
||||
const MONGOAUTH = process.env.MONGOAUTH || false
|
||||
const MONGOUSRP = process.env.MONGOUSRP || ''
|
||||
const MONGOBASE = process.env.MONGOBASE || 'sensor_data'
|
||||
let MONGO_URL = 'mongodb://'+MONGOHOST+':'+MONGOPORT; // URL to mongo database
|
||||
if (MONGOAUTH === 'true') {
|
||||
MONGO_URL = 'mongodb://'+MONGOUSRP+'@' + MONGOHOST + ':' + MONGOPORT + '/?authSource=admin'; // URL to mongo database
|
||||
}
|
||||
const DB_NAME = process.env.DB_NAME || 'espdb';
|
||||
const DB_NAME = MONGOBASE
|
||||
|
||||
let db, entriesCollection, usersCollection;
|
||||
let db, usersCollection, prop_fluxCollection, propertiesCollection;
|
||||
let client = null;
|
||||
|
||||
export async function initMongo() {
|
||||
const client = new MongoClient(MONGO_URI);
|
||||
const client = new MongoClient(MONGO_URL);
|
||||
await client.connect();
|
||||
db = client.db(DB_NAME);
|
||||
entriesCollection = db.collection('entries');
|
||||
usersCollection = db.collection('users');
|
||||
return { db, entriesCollection, usersCollection };
|
||||
usersCollection = db.collection('user');
|
||||
prop_fluxCollection = db.collection('prop_flux');
|
||||
propertiesCollection = db.collection('properties')
|
||||
return { db, usersCollection, prop_fluxCollection, propertiesCollection};
|
||||
}
|
||||
|
||||
export function getCollections() {
|
||||
return { db, entriesCollection, usersCollection };
|
||||
export const clientClose = async () => {
|
||||
if (client) {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getCollections() {
|
||||
return { db, usersCollection, prop_fluxCollection, propertiesCollection};
|
||||
}
|
||||
|
||||
export const update_pflux = async(sn, doc) => {
|
||||
try {
|
||||
await prop_fluxCollection.updateOne({_id: sn},{ $set: { 'chip': doc}})
|
||||
return {"error": null}
|
||||
} catch (e) {
|
||||
return { "error": true, "what": e}
|
||||
}
|
||||
}
|
||||
|
||||
export const get_pflux = async(sn) => {
|
||||
try {
|
||||
let r = await prop_fluxCollection.findOne({_id: sn})
|
||||
if (r == null) {
|
||||
return { "error": true, "what": "Not found", "erg": r}
|
||||
}
|
||||
return {"error": null, "what": null, "erg": r}
|
||||
} catch (e) {
|
||||
return { "error": true, "what": e, "erg": null}
|
||||
}
|
||||
}
|
||||
36
deploy.sh
Executable file
36
deploy.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
# Deplay ein Sensor-File auf das docker registry (docker.citysensor.de)
|
||||
#
|
||||
# v 1.0 2024-09-01 rxf
|
||||
# erste Version
|
||||
|
||||
#set -x
|
||||
|
||||
registry=docker.citysensor.de
|
||||
name=esp2sensor
|
||||
|
||||
|
||||
usage()
|
||||
{
|
||||
echo "Usage ./deploy.sh"
|
||||
echo " Build docker container '$name' and deploy to $registry"
|
||||
echo "Params:"
|
||||
echo " -h show this usage"
|
||||
}
|
||||
|
||||
while getopts h? o
|
||||
do
|
||||
case "$o" in
|
||||
h) usage; exit 0;;
|
||||
*) usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
|
||||
./build_and_copy.sh localhost
|
||||
docker tag $name docker.citysensor.de/$name:latest
|
||||
dat=`date +%Y%m%d%H%M`
|
||||
docker tag $name docker.citysensor.de/$name:V_$dat
|
||||
docker push docker.citysensor.de/$name
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: esp-app
|
||||
container_name: esp2sensor
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- PORT=3000
|
||||
- MONGO_URI=mongodb://mongo:27017
|
||||
- DB_NAME=espdb
|
||||
depends_on:
|
||||
- mongo
|
||||
volumes:
|
||||
- .:/app # bind mount für Live-Reload
|
||||
- /app/node_modules # node_modules vom Host nicht überschreiben
|
||||
- ./log:/var/log
|
||||
restart: unless-stopped
|
||||
|
||||
mongo:
|
||||
|
||||
BIN
docs/ESP2SENSOR_PICS/eingabe.png
Normal file
BIN
docs/ESP2SENSOR_PICS/eingabe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
docs/ESP2SENSOR_PICS/liste.png
Normal file
BIN
docs/ESP2SENSOR_PICS/liste.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
37
docs/beschreibung.md
Normal file
37
docs/beschreibung.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Beschreibung von esp2senor
|
||||
|
||||
|
||||
### Aufruf
|
||||
Für die Tests:
|
||||
|
||||
```
|
||||
esp2sensor.fuerst-stuttgart.de
|
||||
```
|
||||
|
||||
### Darstellung
|
||||
Die **Eingabe**-Seite sieht folgendermaßen aus:
|
||||

|
||||
|
||||
* In die Zeile **Sensornummer** wird die Sensornummer (die von sensor.community) eingetragen. Das Programm sucht daraufhin, ob die Sensornummer in der bisherigen Datenbank bekannt ist. Wenn ja, wird die Adresse aus den Koordinaten gelesen und in der Zeile **Anschrift** angezeigt. Falls nicht, erfolgt eine Fehlermeldung.
|
||||
|
||||
* Nun können die rerstlichen Daten, nämlich ESP-ID, Bezeichnung und Beschreibung eigegeben werden. Nur die ESP_ID **muss** eingegeben werden, die anderen beiden sind optional. Die Anschrift kann *nicht* eingegeben und *nicht* verändert werden.
|
||||
|
||||
* Durch Klick auf **Speichern** werden die Daten in die Datenbank übernommen. Eventuell schon in der DB vorhandene Daten werden überschrieben. Die Anschrift wird *nicht* gespeichert, da diese sich immer aus den schon vohandenen Koordinaten berechnet.
|
||||
|
||||
* Sollte die Sensornummer schon mal eingegeben worden sein, so werden alle eingegebenen Daten aus der Datenbank geholt und angezeigt. Sie können hier nun auch geändert werden (außer Anschrift).
|
||||
|
||||
Für die Speicherung dieser neuen Daten wird eine extra 'Collection' namens *prop_flux* verwendet. Die Koordinaten für die Adresse werden aus der Standard-Collection *properties* geholt. Alle in dieser Collection vohandenen Daten werde aus *properties* nach *prop_flux* kopiert und um die Eingaben ergänzt.
|
||||
Dies ist schon mal eine Vorebreitung für eine zukünftige Sensor-Verwaltung.
|
||||
|
||||
Die zweite Seite ist die **Liste**:
|
||||

|
||||
|
||||
Je mehr Daten vorliegen, desto länger wird die Liste. Über **Aktualisieren** kann die Liste neu geladen werden. Über **Seite** kann die angezeigte Seite angewählt werde, über **Limit** wird die Anzahl der Einträge einer Seite festgelegt.
|
||||
Angezeigt werden die eingegebenen Daten aus der Collecction *prop_flux*. Es wird nach dem Eingabe-Datum absteigend sortiert.
|
||||
|
||||
#### History
|
||||
| Version | Datum | Author |Bemerkung|
|
||||
|---------|-------|-----|----------|
|
||||
|1.0.0 | 2025-08-19 | rxf | erste Fassung |
|
||||
|
||||
|
||||
3
hashpasswd
Normal file
3
hashpasswd
Normal file
@@ -0,0 +1,3 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
const hashedPassword = await bcrypt.hash('Tux4esp', 10);
|
||||
console.log(hashedPassword)
|
||||
311
log/esp2sensor.log
Normal file
311
log/esp2sensor.log
Normal file
@@ -0,0 +1,311 @@
|
||||
[dotenv@17.2.1] injecting env (10) from .env -- tip: 🛠️ run anywhere with `dotenvx run -- yourcommand`
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: 🔐 prevent building .env in docker: https://dotenvx.com/prebuild
|
||||
/app/node_modules/mongodb/lib/sdam/topology.js:326
|
||||
const timeoutError = new error_1.MongoServerSelectionError(`Server selection timed out after ${timeout?.duration} ms`, this.description);
|
||||
^
|
||||
|
||||
MongoServerSelectionError: connect ECONNREFUSED 192.168.16.2:2017
|
||||
at Topology.selectServer (/app/node_modules/mongodb/lib/sdam/topology.js:326:38)
|
||||
at async Topology._connect (/app/node_modules/mongodb/lib/sdam/topology.js:200:28)
|
||||
at async Topology.connect (/app/node_modules/mongodb/lib/sdam/topology.js:152:13)
|
||||
at async topologyConnect (/app/node_modules/mongodb/lib/mongo_client.js:258:17)
|
||||
at async MongoClient._connect (/app/node_modules/mongodb/lib/mongo_client.js:271:13)
|
||||
at async MongoClient.connect (/app/node_modules/mongodb/lib/mongo_client.js:196:13)
|
||||
at async initMongo (file:///app/db/mongo.js:21:3)
|
||||
at async file:///app/server.js:35:1 {
|
||||
errorLabelSet: Set(0) {},
|
||||
reason: TopologyDescription {
|
||||
type: 'Unknown',
|
||||
servers: Map(1) {
|
||||
'mongo:2017' => ServerDescription {
|
||||
address: 'mongo:2017',
|
||||
type: 'Unknown',
|
||||
hosts: [],
|
||||
passives: [],
|
||||
arbiters: [],
|
||||
tags: {},
|
||||
minWireVersion: 0,
|
||||
maxWireVersion: 0,
|
||||
roundTripTime: -1,
|
||||
minRoundTripTime: 0,
|
||||
lastUpdateTime: 72112013,
|
||||
lastWriteDate: 0,
|
||||
error: MongoNetworkError: connect ECONNREFUSED 192.168.16.2:2017
|
||||
at Socket.<anonymous> (/app/node_modules/mongodb/lib/cmap/connect.js:286:44)
|
||||
at Object.onceWrapper (node:events:639:26)
|
||||
at Socket.emit (node:events:524:28)
|
||||
at emitErrorNT (node:internal/streams/destroy:169:8)
|
||||
at emitErrorCloseNT (node:internal/streams/destroy:128:3)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
|
||||
errorLabelSet: Set(1) { 'ResetPool' },
|
||||
beforeHandshake: false,
|
||||
[cause]: Error: connect ECONNREFUSED 192.168.16.2:2017
|
||||
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1611:16) {
|
||||
errno: -111,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '192.168.16.2',
|
||||
port: 2017
|
||||
}
|
||||
},
|
||||
topologyVersion: null,
|
||||
setName: null,
|
||||
setVersion: null,
|
||||
electionId: null,
|
||||
logicalSessionTimeoutMinutes: null,
|
||||
maxMessageSizeBytes: null,
|
||||
maxWriteBatchSize: null,
|
||||
maxBsonObjectSize: null,
|
||||
primary: null,
|
||||
me: null,
|
||||
'$clusterTime': null,
|
||||
iscryptd: false
|
||||
}
|
||||
},
|
||||
stale: false,
|
||||
compatible: true,
|
||||
heartbeatFrequencyMS: 10000,
|
||||
localThresholdMS: 15,
|
||||
setName: null,
|
||||
maxElectionId: null,
|
||||
maxSetVersion: null,
|
||||
commonWireVersion: 0,
|
||||
logicalSessionTimeoutMinutes: null
|
||||
},
|
||||
code: undefined,
|
||||
[cause]: MongoNetworkError: connect ECONNREFUSED 192.168.16.2:2017
|
||||
at Socket.<anonymous> (/app/node_modules/mongodb/lib/cmap/connect.js:286:44)
|
||||
at Object.onceWrapper (node:events:639:26)
|
||||
at Socket.emit (node:events:524:28)
|
||||
at emitErrorNT (node:internal/streams/destroy:169:8)
|
||||
at emitErrorCloseNT (node:internal/streams/destroy:128:3)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
|
||||
errorLabelSet: Set(1) { 'ResetPool' },
|
||||
beforeHandshake: false,
|
||||
[cause]: Error: connect ECONNREFUSED 192.168.16.2:2017
|
||||
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1611:16) {
|
||||
errno: -111,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '192.168.16.2',
|
||||
port: 2017
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
[dotenv@17.2.1] injecting env (10) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ override existing env vars with { override: true }
|
||||
[dotenv@17.2.1] injecting env (10) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: 📡 version env with Radar: https://dotenvx.com/radar
|
||||
Server läuft auf http://localhost:3000
|
||||
Address lookup failed: TypeError: fetch failed
|
||||
at node:internal/deps/undici/undici:13510:13
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||
at async file:///app/routes/address.js:44:19 {
|
||||
[cause]: Error: getaddrinfo EAI_AGAIN sensorapi
|
||||
at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:120:26) {
|
||||
errno: -3001,
|
||||
code: 'EAI_AGAIN',
|
||||
syscall: 'getaddrinfo',
|
||||
hostname: 'sensorapi'
|
||||
}
|
||||
}
|
||||
file:///app/routes/address.js:59
|
||||
await client.close();
|
||||
^
|
||||
|
||||
ReferenceError: client is not defined
|
||||
at file:///app/routes/address.js:59:5
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||
|
||||
Node.js v20.19.4
|
||||
[dotenv@17.2.1] injecting env (10) from .env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ load multiple .env files with { path: ['.env.local', '.env'] }
|
||||
Server läuft auf http://localhost:3000
|
||||
Address lookup failed: TypeError: fetch failed
|
||||
at node:internal/deps/undici/undici:13510:13
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||
at async file:///app/routes/address.js:44:19 {
|
||||
[cause]: Error: getaddrinfo EAI_AGAIN sensorapi
|
||||
at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:120:26) {
|
||||
errno: -3001,
|
||||
code: 'EAI_AGAIN',
|
||||
syscall: 'getaddrinfo',
|
||||
hostname: 'sensorapi'
|
||||
}
|
||||
}
|
||||
file:///app/routes/address.js:59
|
||||
await client.close();
|
||||
^
|
||||
|
||||
ReferenceError: client is not defined
|
||||
at file:///app/routes/address.js:59:5
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||
|
||||
Node.js v20.19.4
|
||||
[dotenv@17.2.1] injecting env (10) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ load multiple .env files with { path: ['.env.local', '.env'] }
|
||||
Server läuft auf http://localhost:3000
|
||||
[dotenv@17.2.1] injecting env (9) from .env -- tip: 🔐 prevent building .env in docker: https://dotenvx.com/prebuild
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ enable debug logging with { debug: true }
|
||||
Server läuft auf http://localhost:3000
|
||||
[dotenv@17.2.1] injecting env (9) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
|
||||
Server läuft auf http://localhost:3000
|
||||
[dotenv@17.2.1] injecting env (9) from .env -- tip: ⚙️ suppress all logs with { quiet: true }
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ enable debug logging with { debug: true }
|
||||
Server läuft auf http://localhost:3000
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddressgetaddress/?sensorid=37833
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddressgetaddress/?sensorid=37833
|
||||
[dotenv@17.2.1] injecting env (9) from .env -- tip: ⚙️ write to custom object with { processEnv: myObject }
|
||||
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ write to custom object with { processEnv: myObject }
|
||||
Server läuft auf http://localhost:3000
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=37833
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=37833
|
||||
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=37833
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
node:internal/modules/cjs/loader:1215
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/app/server.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:1043:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
|
||||
at node:internal/main/run_main_module:28:49 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v20.19.4
|
||||
1321
package-lock.json
generated
1321
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "espid2sensor",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"date": "2025-09-02 17:00 UTC",
|
||||
"type": "module",
|
||||
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start": "node server.js >>/var/log/esp2sensor.log 2>&1",
|
||||
"dev": "nodemon --watch server.js --watch views --watch public server.js",
|
||||
"test": "jest"
|
||||
},
|
||||
@@ -13,14 +14,14 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^4.18.2",
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.2",
|
||||
"mongodb": "^6.6.0",
|
||||
"pug": "^3.0.2"
|
||||
"mongodb": "^6.19.0",
|
||||
"pug": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^30.0.5",
|
||||
"nodemon": "^3.0.1",
|
||||
"jest": "^30.1.3",
|
||||
"nodemon": "^3.1.10",
|
||||
"supertest": "^7.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
401
public/global.js
401
public/global.js
@@ -1,3 +1,65 @@
|
||||
// Tab-Wechsel Funktion aus index.pug
|
||||
function showTab(tab) {
|
||||
document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none';
|
||||
document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none';
|
||||
const tabUserContent = document.getElementById('tabUserContent');
|
||||
if (tabUserContent) tabUserContent.style.display = tab === 'user' ? '' : 'none';
|
||||
document.getElementById('tabInput').classList.toggle('active', tab === 'input');
|
||||
document.getElementById('tabList').classList.toggle('active', tab === 'list');
|
||||
const tabUser = document.getElementById('tabUser');
|
||||
if (tabUser) tabUser.classList.toggle('active', tab === 'user');
|
||||
}
|
||||
|
||||
// User-Tab Handling (nur für Admins)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const userSaveBtn = document.getElementById('userSaveBtn');
|
||||
if (userSaveBtn) {
|
||||
userSaveBtn.addEventListener('click', async () => {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const role = document.getElementById('role').value;
|
||||
const userResult = document.getElementById('userResult');
|
||||
if (!username || !password) {
|
||||
userResult.textContent = 'Benutzername und Passwort erforderlich.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/createUser', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, role })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
userResult.textContent = 'User erfolgreich angelegt!';
|
||||
} else {
|
||||
userResult.textContent = data.error || 'Fehler beim Anlegen.';
|
||||
}
|
||||
} catch (err) {
|
||||
userResult.textContent = 'Serverfehler.';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function updateSortArrows() {
|
||||
const arrows = {
|
||||
sensorNr: document.getElementById('sortArrowSensorNr'),
|
||||
espId: document.getElementById('sortArrowEspId'),
|
||||
date: document.getElementById('sortArrowDate')
|
||||
};
|
||||
Object.entries(arrows).forEach(([key, el]) => {
|
||||
if (!el) return;
|
||||
// Aktiver Pfeil fett, andere ausgegraut
|
||||
el.textContent = currentSort.key === key
|
||||
? (currentSort.asc ? '↑' : '↓')
|
||||
: '↑';
|
||||
el.style.fontWeight = currentSort.key === key ? 'bold' : 'normal';
|
||||
el.style.opacity = currentSort.key === key ? '1' : '0.3';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
@@ -10,83 +72,126 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const limitInput = document.getElementById('limit');
|
||||
const resultDiv = document.getElementById('result');
|
||||
const tableBody = document.querySelector('#entriesTable tbody');
|
||||
const tabInput = document.getElementById('tabInput');
|
||||
const tabList = document.getElementById('tabList');
|
||||
|
||||
let editId = null;
|
||||
|
||||
// Modal für Fehleranzeige
|
||||
function showModal(message, callback) {
|
||||
// Vorherige Modals entfernen
|
||||
document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
|
||||
function showModal(message, showCancelButton, callback) {
|
||||
// Remove previous modals
|
||||
document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
|
||||
|
||||
let modal = document.createElement('div');
|
||||
modal.className = 'custom-modal-popup';
|
||||
let modal = document.createElement('div');
|
||||
modal.className = 'custom-modal-popup';
|
||||
|
||||
let box = document.createElement('div');
|
||||
box.className = 'custom-modal-box';
|
||||
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 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 = () => {
|
||||
let btndiv = document.createElement('div')
|
||||
btndiv.className = 'twobuttons'
|
||||
|
||||
// Cancel Button (only if showCancelButton is true)
|
||||
if (showCancelButton) {
|
||||
let btnCancel = document.createElement('button');
|
||||
btnCancel.className = 'custom-modal-btn';
|
||||
btnCancel.textContent = 'Abbruch';
|
||||
btnCancel.onclick = () => {
|
||||
if (modal.parentNode) {
|
||||
modal.parentNode.removeChild(modal);
|
||||
}
|
||||
if (callback) callback();
|
||||
if (callback) callback(false); // Pass false for Cancel
|
||||
};
|
||||
box.appendChild(btn);
|
||||
modal.appendChild(box);
|
||||
document.body.appendChild(modal);
|
||||
btndiv.appendChild(btnCancel);
|
||||
}
|
||||
|
||||
// Sensornummer nur Zahlen erlauben
|
||||
sensorNumberInput.addEventListener('input', () => {
|
||||
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
|
||||
});
|
||||
// OK Button
|
||||
let btnOk = document.createElement('button');
|
||||
btnOk.className = 'custom-modal-btn';
|
||||
btnOk.textContent = 'OK';
|
||||
btnOk.onclick = () => {
|
||||
if (modal.parentNode) {
|
||||
modal.parentNode.removeChild(modal);
|
||||
}
|
||||
if (callback) callback(true); // Pass true for OK
|
||||
};
|
||||
|
||||
// Adresse vom Server holen, wenn Enter oder Feld verlassen
|
||||
async function fetchAddressIfValid() {
|
||||
const value = sensorNumberInput.value.trim();
|
||||
if (value.length > 0) {
|
||||
try {
|
||||
const res = await fetch(`/api/address/${value}`);
|
||||
const data = await res.json();
|
||||
if (!data.error && data.address) {
|
||||
addressInput.value = data.address;
|
||||
} else {
|
||||
btndiv.appendChild(btnOk);
|
||||
box.appendChild(btndiv)
|
||||
|
||||
|
||||
modal.appendChild(box);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Optional: Close modal when clicking outside
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
if (modal.parentNode) {
|
||||
modal.parentNode.removeChild(modal);
|
||||
}
|
||||
if (callback) callback(false); // Treat as cancel
|
||||
}
|
||||
};
|
||||
}
|
||||
// Sensornummer nur Zahlen erlauben
|
||||
sensorNumberInput.addEventListener('input', () => {
|
||||
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
|
||||
});
|
||||
|
||||
// Adresse vom Server holen, wenn Enter oder Feld verlassen
|
||||
async function fetchAddressIfValid() {
|
||||
const value = sensorNumberInput.value.trim();
|
||||
if (value.length > 0) {
|
||||
try {
|
||||
const res = await fetch(`/api/address/${value}`);
|
||||
const data = await res.json();
|
||||
console.dir(data)
|
||||
if (!data.error && data.address) {
|
||||
addressInput.value = data.address;
|
||||
// Felder automatisch füllen, wenn props vorhanden
|
||||
if (!data.props.error) {
|
||||
if (data.props.erg.chip !== undefined) {
|
||||
let pp = data.props.erg.chip
|
||||
espIdInput.value = pp.id || ''
|
||||
nameInput.value = pp.name || ''
|
||||
descriptionInput.value = pp.description || ''
|
||||
// Weitere Felder nach Bedarf
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addressInput.value = '';
|
||||
sensorNumberInput.disabled = true;
|
||||
showModal('Sensor unbekannt', false, () => {
|
||||
sensorNumberInput.disabled = false;
|
||||
sensorNumberInput.focus();
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Abrufen der Adresse:', err);
|
||||
addressInput.value = '';
|
||||
sensorNumberInput.disabled = true;
|
||||
showModal('Sensor unbekannt', () => {
|
||||
showModal('Sensor unbekannt', false, () => {
|
||||
sensorNumberInput.disabled = false;
|
||||
sensorNumberInput.focus();
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Abrufen der Adresse:', err);
|
||||
addressInput.value = '';
|
||||
sensorNumberInput.disabled = true;
|
||||
showModal('Sensor unbekannt', () => {
|
||||
sensorNumberInput.disabled = false;
|
||||
sensorNumberInput.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enter-Taste
|
||||
sensorNumberInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
fetchAddressIfValid();
|
||||
}
|
||||
});
|
||||
// Enter-Taste
|
||||
sensorNumberInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
fetchAddressIfValid();
|
||||
}
|
||||
});
|
||||
|
||||
// Feld verlassen
|
||||
sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
// Feld verlassen
|
||||
sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
|
||||
|
||||
async function saveEntry() {
|
||||
@@ -102,8 +207,8 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
}
|
||||
|
||||
try {
|
||||
const url = editId ? `/api/update/${editId}` : '/api/save';
|
||||
const method = editId ? 'PUT' : 'POST';
|
||||
const url = '/api/save';
|
||||
const method = 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
@@ -115,8 +220,12 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
if (data.error) {
|
||||
resultDiv.textContent = data.error;
|
||||
} else {
|
||||
resultDiv.textContent = editId ? 'Aktualisiert!' : 'Gespeichert!';
|
||||
clearForm();
|
||||
resultDiv.textContent = 'OK!';
|
||||
setTimeout(() => {
|
||||
resultDiv.textContent = ''
|
||||
saveBtn.textContent = 'Speichern';
|
||||
}, 5000)
|
||||
clearForm(false);
|
||||
await loadEntries();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -125,42 +234,112 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
}
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
|
||||
function clearForm(mitButton) {
|
||||
espIdInput.value = '';
|
||||
sensorNumberInput.value = '';
|
||||
nameInput.value = '';
|
||||
descriptionInput.value = '';
|
||||
addressInput.value = '';
|
||||
editId = null;
|
||||
saveBtn.textContent = 'Speichern';
|
||||
if (mitButton) {
|
||||
saveBtn.textContent = 'Speichern';
|
||||
}
|
||||
}
|
||||
|
||||
const clearUserForm = () => {
|
||||
document.getElementById('username').value = ''
|
||||
document.getElementById('password').value = ''
|
||||
document.getElementById('role').value = 'user'
|
||||
}
|
||||
|
||||
// Globale Sortier-Variable
|
||||
window.currentSort = window.currentSort || { key: null, asc: true };
|
||||
|
||||
async function loadEntries() {
|
||||
const page = parseInt(pageInput.value) || 1;
|
||||
const limit = parseInt(limitInput.value) || 10;
|
||||
const limit = parseInt(limitInput.value) || 50;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/list?page=${page}&limit=${limit}`);
|
||||
const items = await res.json();
|
||||
const erg = await res.json();
|
||||
const items = erg.items
|
||||
const gz = document.getElementById('gzahl');
|
||||
gz.innerHTML = `Gesamtzahl: ${erg.anzahl}`
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
items.forEach(item => {
|
||||
const date = new Date(item.createdAt).toISOString().split('T')[0];
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.espId}</td>
|
||||
<td>${item.sensorNumber}</td>
|
||||
<td>${item.name || ''}</td>
|
||||
<td>${item.description || ''}</td>
|
||||
<td>${item.address || ''}</td>
|
||||
<td>${date}</td>
|
||||
<td>
|
||||
<button data-id="${item._id}" class="editBtn">Bearbeiten</button>
|
||||
<button data-id="${item._id}" class="deleteBtn">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
let currentSort = window.currentSort || { key: null, asc: true };
|
||||
function renderTable(sortedItems) {
|
||||
tableBody.innerHTML = '';
|
||||
sortedItems.forEach(item => {
|
||||
const date = new Date(item.chip.lastUpdatedAt).toISOString().split('T')[0];
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td id="tdSensornumber">${item._id}</td>
|
||||
<td>${item.chip.id}</td>
|
||||
<td>${item.chip.name || ''}</td>
|
||||
<td id="tdBeschreibung">${item.chip.description || ''}</td>
|
||||
<td id="tdDate">${date}</td>
|
||||
<td>
|
||||
<div class="twobuttons">
|
||||
<button data-id="${item._id}" class="editBtn" title="Bearbeiten">✏️</button>
|
||||
<button data-id="${item._id}" class="deleteBtn" title="Löschen">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function sortItems(items, key, asc) {
|
||||
return items.slice().sort((a, b) => {
|
||||
let valA, valB;
|
||||
if (key === 'sensorNr') {
|
||||
valA = a._id;
|
||||
valB = b._id;
|
||||
} else if (key === 'espId') {
|
||||
valA = a.chip.id;
|
||||
valB = b.chip.id;
|
||||
} else if (key === 'date') {
|
||||
valA = new Date(a.chip.lastUpdatedAt);
|
||||
valB = new Date(b.chip.lastUpdatedAt);
|
||||
}
|
||||
if (valA < valB) return asc ? -1 : 1;
|
||||
if (valA > valB) return asc ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Initial render: Standard nach SensorNr, ESP-ID oder Datum aufsteigend
|
||||
// Ändere hier die Spalte für die Standardsortierung:
|
||||
const defaultSortKey = window.currentSort && window.currentSort.key ? window.currentSort.key : 'sensorNr';
|
||||
const defaultSortAsc = window.currentSort && typeof window.currentSort.asc === 'boolean' ? window.currentSort.asc : true;
|
||||
currentSort.key = defaultSortKey;
|
||||
currentSort.asc = defaultSortAsc;
|
||||
window.currentSort = currentSort;
|
||||
renderTable(sortItems(items, defaultSortKey, defaultSortAsc));
|
||||
updateSortArrows();
|
||||
|
||||
// Add sort listeners
|
||||
document.getElementById('thSensorNr').onclick = () => {
|
||||
currentSort.asc = currentSort.key === 'sensorNr' ? !currentSort.asc : true;
|
||||
currentSort.key = 'sensorNr';
|
||||
window.currentSort = currentSort;
|
||||
renderTable(sortItems(items, 'sensorNr', currentSort.asc));
|
||||
updateSortArrows();
|
||||
};
|
||||
document.getElementById('thEspId').onclick = () => {
|
||||
currentSort.asc = currentSort.key === 'espId' ? !currentSort.asc : true;
|
||||
currentSort.key = 'espId';
|
||||
window.currentSort = currentSort;
|
||||
renderTable(sortItems(items, 'espId', currentSort.asc));
|
||||
updateSortArrows();
|
||||
};
|
||||
document.getElementById('thDate').onclick = () => {
|
||||
currentSort.asc = currentSort.key === 'date' ? !currentSort.asc : true;
|
||||
currentSort.key = 'date';
|
||||
window.currentSort = currentSort;
|
||||
renderTable(sortItems(items, 'date', currentSort.asc));
|
||||
updateSortArrows();
|
||||
};
|
||||
|
||||
document.querySelectorAll('.deleteBtn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
@@ -174,15 +353,25 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
const id = btn.getAttribute('data-id');
|
||||
const res = await fetch(`/api/list?page=1&limit=1&id=${id}`);
|
||||
const items = await res.json();
|
||||
const item = items.find(e => e._id === id);
|
||||
const item = items.find(e => e._id === parseInt(id));
|
||||
if (item) {
|
||||
espIdInput.value = item.espId;
|
||||
sensorNumberInput.value = item.sensorNumber;
|
||||
nameInput.value = item.name || '';
|
||||
descriptionInput.value = item.description || '';
|
||||
addressInput.value = item.address || '';
|
||||
editId = id;
|
||||
espIdInput.value = item.chip.id;
|
||||
sensorNumberInput.value = item._id;
|
||||
nameInput.value = item.chip.name || '';
|
||||
descriptionInput.value = item.chip.description || '';
|
||||
addressInput.value = '';
|
||||
saveBtn.textContent = 'Aktualisieren';
|
||||
showTab('input')
|
||||
try {
|
||||
const rt = await fetch(`api/holAdresse/${item._id}`)
|
||||
const data = await rt.json();
|
||||
console.dir(data)
|
||||
if (!data.error && data.address) {
|
||||
addressInput.value = data.address;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Fehler beim Adresse holen", e)
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -193,21 +382,37 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
}
|
||||
|
||||
async function deleteEntry(id) {
|
||||
if (!confirm('Wirklich löschen?')) return;
|
||||
try {
|
||||
const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
await loadEntries();
|
||||
showModal('Wirklich löschen?', true, async (confirmed) => {
|
||||
if (confirmed) {
|
||||
try {
|
||||
const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
await loadEntries();
|
||||
resultDiv.textContent = 'Eintrag gelöscht.';
|
||||
setTimeout(() => resultDiv.textContent = '', 3000);
|
||||
} else {
|
||||
resultDiv.textContent = 'Fehler beim Löschen.';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Löschen.';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Löschen.';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveBtn.addEventListener('click', saveEntry);
|
||||
refreshBtn.addEventListener('click', loadEntries);
|
||||
cancelBtn.addEventListener('click', () => clearForm(true));
|
||||
userCancelBtn.addEventListener('click', () => clearUserForm(true));
|
||||
|
||||
tabInput.addEventListener('click', () => showTab('input'))
|
||||
tabList.addEventListener('click', () => showTab('list'))
|
||||
const tabUser = document.getElementById('tabUser');
|
||||
if (tabUser) tabUser.addEventListener('click', () => showTab('user'))
|
||||
|
||||
loadEntries();
|
||||
});
|
||||
});
|
||||
|
||||
window.showTab = showTab;
|
||||
@@ -1,37 +0,0 @@
|
||||
// public/register.js
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const emailInput = document.getElementById('email');
|
||||
const emailStatus = document.getElementById('emailStatus');
|
||||
|
||||
let debounceTimeout;
|
||||
|
||||
emailInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimeout);
|
||||
const email = emailInput.value.trim();
|
||||
|
||||
if (!email) {
|
||||
emailStatus.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 300ms warten, um zu vermeiden, dass bei jedem Tastendruck eine Anfrage rausgeht
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`);
|
||||
const data = await res.json();
|
||||
if (data.exists) {
|
||||
emailStatus.textContent = '❌ Diese E-Mail ist schon vergeben';
|
||||
emailStatus.style.color = 'red';
|
||||
} else {
|
||||
emailStatus.textContent = '✅ E-Mail ist frei';
|
||||
emailStatus.style.color = 'green';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
emailStatus.textContent = 'Fehler bei der Prüfung';
|
||||
emailStatus.style.color = 'orange';
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,11 @@
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
|
||||
}
|
||||
|
||||
#tabUser {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
/* Modal Fehlerfenster */
|
||||
.custom-modal-popup {
|
||||
position: fixed;
|
||||
@@ -95,14 +100,24 @@ input, button {
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid #888;
|
||||
}
|
||||
|
||||
/* Spaltenbreiten über colgroup steuern */
|
||||
col.col-sensornumber { width: 7em; }
|
||||
col.col-espid {width: 9em}
|
||||
col.col-bezeichnung { width: 8em; }
|
||||
col.col-beschreibung{ width: 15em; }
|
||||
col.col-date { width: 10em; }
|
||||
col.col-aktionen { width: 2em; }
|
||||
|
||||
|
||||
.controls input#page,
|
||||
.controls input#limit {
|
||||
width: 50px;
|
||||
@@ -151,10 +166,33 @@ button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
#saveBtn {
|
||||
margin-top: 20px;
|
||||
.editBtn, .deleteBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.editBtn:hover {
|
||||
background: rgba(0, 123, 255, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.deleteBtn:hover {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.twobuttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 5px;
|
||||
}
|
||||
p.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
@@ -184,4 +222,23 @@ p.error {
|
||||
.card form textarea {
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#gzahl {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
#role {
|
||||
font-size: 12pt;
|
||||
padding: 5px 0 5px 3px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#version {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
font-size: 70%;
|
||||
color: #007bff;
|
||||
margin-top: 15px;
|
||||
}
|
||||
@@ -1,33 +1,82 @@
|
||||
import { getCollections } from '../db/mongo.js';
|
||||
import { MongoClient } from 'mongodb';
|
||||
import { get_pflux } from '../db/mongo.js';
|
||||
import { getCollections, update_pflux, clientClose } from '../db/mongo.js';
|
||||
|
||||
export function registerAddressRoute(app, requireLogin) {
|
||||
const ADDRESS_SERVICE_URL = process.env.ADDRESS_SERVICE_URL || 'https://noise.fuerst-stuttgart.de/srv/getaddress';
|
||||
const APIHOST = process.env.APIHOST || 'https://noise.fuerst-stuttgart.de/srv/';
|
||||
|
||||
|
||||
const holAdresse = async (id) => {
|
||||
// Adresse wie bisher holen (über die Sensornummer via nominative)
|
||||
let addressString = '';
|
||||
let addrParts = {};
|
||||
try {
|
||||
const url = APIHOST + 'getaddress/' + `?sensorid=${id}`;
|
||||
console.log(url)
|
||||
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
const addrObj = data?.erg?.address || data?.address || {};
|
||||
const street = addrObj.street ?? '';
|
||||
const plz = addrObj.plz ?? '';
|
||||
const city = addrObj.city ?? '';
|
||||
const rightPart = [plz, city].filter(Boolean).join(' ').trim();
|
||||
addressString = [street, rightPart].filter(Boolean).join(', ');
|
||||
addrParts = { street, plz, city };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Address lookup failed:', err);
|
||||
}
|
||||
return {
|
||||
address: addressString,
|
||||
parts: addrParts,
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/api/holAdresse/:sensorNumber', requireLogin, async (req, res) => {
|
||||
const sensorNumber = parseInt(req.params.sensorNumber, 10);
|
||||
if (isNaN(sensorNumber)) {
|
||||
return res.status(400).json({ error: 'Ungültige Sensornummer' });
|
||||
}
|
||||
const addr = await holAdresse(sensorNumber)
|
||||
res.json(addr)
|
||||
})
|
||||
|
||||
app.get('/api/address/:sensorNumber', requireLogin, async (req, res) => {
|
||||
const sensorNumber = parseInt(req.params.sensorNumber, 10);
|
||||
if (isNaN(sensorNumber)) {
|
||||
return res.status(400).json({ error: 'Ungültige Sensornummer' });
|
||||
}
|
||||
const url = ADDRESS_SERVICE_URL + `?sensorid=${encodeURIComponent(sensorNumber)}`;
|
||||
try {
|
||||
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
||||
if (!r.ok) {
|
||||
return res.status(502).json({ error: `Adressdienst Fehler (${r.status})` });
|
||||
}
|
||||
const data = await r.json();
|
||||
const addrObj = data?.erg?.address || data?.address || {};
|
||||
const street = addrObj.street ?? '';
|
||||
const plz = addrObj.plz ?? '';
|
||||
const city = addrObj.city ?? '';
|
||||
const rightPart = [plz, city].filter(Boolean).join(' ').trim();
|
||||
const addressString = [street, rightPart].filter(Boolean).join(', ');
|
||||
return res.json({
|
||||
address: addressString,
|
||||
parts: { street, plz, city }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Address lookup failed:', err);
|
||||
return res.status(504).json({ error: 'Adressdienst nicht erreichbar' });
|
||||
|
||||
const { propertiesCollection, prop_fluxCollection } = getCollections();
|
||||
|
||||
// Suche nach Sensornummer als _id
|
||||
const propEntry = await propertiesCollection.findOne({ _id: sensorNumber });
|
||||
if (!propEntry) {
|
||||
await clientClose()
|
||||
return res.status(404).json({ error: 'Sensor nicht gefunden' });
|
||||
}
|
||||
});
|
||||
|
||||
// Sensor bekannt -> Wurde der schon rüber kopiert?
|
||||
const propsF = await get_pflux(sensorNumber)
|
||||
if(!propsF.erg) {
|
||||
// nein, also kopieren nach prop_flux mit sensorNumber als _id
|
||||
try {
|
||||
await prop_fluxCollection.replaceOne(
|
||||
{ _id: sensorNumber },
|
||||
{ ...propEntry, _id: sensorNumber },
|
||||
{ upsert: true }
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Kopieren nach prop_flux:', err);
|
||||
// Kein Abbruch, nur Logging
|
||||
} finally {
|
||||
await clientClose()
|
||||
}
|
||||
}
|
||||
const adr = await holAdresse(encodeURIComponent(propEntry._id))
|
||||
adr.props = propsF
|
||||
|
||||
return res.json(adr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { MongoAPIError, ObjectId } from 'mongodb';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { getCollections } from '../db/mongo.js';
|
||||
import { getCollections, update_pflux } from '../db/mongo.js';
|
||||
|
||||
export function registerApiRoutes(app, requireLogin) {
|
||||
const { entriesCollection, usersCollection } = getCollections();
|
||||
const { usersCollection, prop_fluxCollection } = getCollections();
|
||||
|
||||
app.get('/api/check-email', async (req, res) => {
|
||||
const email = (req.query.email || '').toLowerCase().trim();
|
||||
if (!email) return res.json({ exists: false });
|
||||
try {
|
||||
const existingUser = await usersCollection.findOne({ email });
|
||||
const existingUser = await usersCollection.findOne({ email:`${email}` });
|
||||
res.json({ exists: !!existingUser });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -18,21 +18,19 @@ export function registerApiRoutes(app, requireLogin) {
|
||||
});
|
||||
|
||||
app.post('/api/save', requireLogin, async (req, res) => {
|
||||
let { espId, sensorNumber, name, description, address } = req.body;
|
||||
let { espId, sensorNumber, name, description} = req.body;
|
||||
if (!espId || !sensorNumber) {
|
||||
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
|
||||
}
|
||||
sensorNumber = parseInt(sensorNumber, 10);
|
||||
try {
|
||||
const doc = {
|
||||
espId,
|
||||
sensorNumber,
|
||||
id: espId,
|
||||
name: name || '',
|
||||
description: description || '',
|
||||
address: address || '',
|
||||
createdAt: new Date()
|
||||
lastUpdatedAt: new Date()
|
||||
};
|
||||
await entriesCollection.insertOne(doc);
|
||||
await update_pflux(sensorNumber, doc)
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -40,30 +38,12 @@ export function registerApiRoutes(app, requireLogin) {
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/update/:id', requireLogin, async (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);
|
||||
try {
|
||||
await entriesCollection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: { espId, sensorNumber, name, description, address } }
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler beim Aktualisieren' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/list', requireLogin, async (req, res) => {
|
||||
const { id } = req.query;
|
||||
if (id) {
|
||||
try {
|
||||
const item = await entriesCollection.findOne({ _id: new ObjectId(id) });
|
||||
const item = await prop_fluxCollection.findOne({ _id: parseInt(id) });
|
||||
if (item) return res.json([item]);
|
||||
return res.json([]);
|
||||
} catch (err) {
|
||||
@@ -71,16 +51,24 @@ export function registerApiRoutes(app, requireLogin) {
|
||||
return res.status(500).json({ error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
let gesamtZahl = 0
|
||||
try {
|
||||
gesamtZahl = await prop_fluxCollection.countDocuments({chip: {$exists: true}})
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const skip = (page - 1) * limit;
|
||||
try {
|
||||
const items = await entriesCollection.find({})
|
||||
.sort({ createdAt: -1 })
|
||||
const items = await prop_fluxCollection.find({chip: {$exists: true}})
|
||||
.sort({ "chip.lastUpdatedAt": -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
res.json(items);
|
||||
const data = {items: items, anzahl: gesamtZahl}
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler beim Laden' });
|
||||
@@ -88,7 +76,20 @@ export function registerApiRoutes(app, requireLogin) {
|
||||
});
|
||||
|
||||
app.delete('/api/delete/:id', requireLogin, async (req, res) => {
|
||||
await entriesCollection.deleteOne({ _id: new ObjectId(req.params.id) });
|
||||
await prop_fluxCollection.deleteOne({ _id: parseInt(req.params.id) });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.post('/api/createUser', requireLogin, async (req, res) => {
|
||||
if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' });
|
||||
const { username, password, role } = req.body;
|
||||
if (!username || !password) return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' });
|
||||
try {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
await usersCollection.insertOne({ email: username.toLowerCase(), passwordHash: hash, role: role || 'user' });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Fehler beim Anlegen' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { getCollections } from '../db/mongo.js';
|
||||
import pkg from '../package.json' with { type: "json" }
|
||||
|
||||
export function registerAuthRoutes(app) {
|
||||
const { usersCollection } = getCollections();
|
||||
const errText = 'Falsche Email oder falsches Passwort.'
|
||||
|
||||
app.get('/register', (req, res) => res.render('register', { error: null }));
|
||||
|
||||
app.post('/register', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
if (!email || !password) return res.render('register', { error: 'Bitte Email und Passwort angeben.' });
|
||||
const existingUser = await usersCollection.findOne({ email: email.toLowerCase() });
|
||||
if (existingUser) return res.render('register', { error: 'Email schon registriert.' });
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
await usersCollection.insertOne({ email: email.toLowerCase(), passwordHash: hash });
|
||||
res.redirect('/login');
|
||||
app.get('/login', (req, res) => {
|
||||
const version = pkg.version
|
||||
const vdate = pkg.date
|
||||
res.render('login', {
|
||||
error: null,
|
||||
version: version,
|
||||
vdate: vdate
|
||||
})
|
||||
});
|
||||
|
||||
app.get('/login', (req, res) => res.render('login', { error: null }));
|
||||
|
||||
app.post('/login', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
const user = await usersCollection.findOne({ email: email.toLowerCase() });
|
||||
if (!user) return res.render('login', { error: 'Falsche Email oder Passwort.' });
|
||||
if (!user) return res.render('login', { error: errText });
|
||||
const match = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!match) return res.render('login', { error: 'Falsche Email oder Passwort.' });
|
||||
req.session.userId = user._id;
|
||||
res.redirect('/');
|
||||
if (!match) return res.render('login', { error: errText });
|
||||
req.session.userId = user._id;
|
||||
req.session.isAdmin = user.role === 'admin';
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
app.get('/logout', (req, res) => {
|
||||
|
||||
15
server.js
15
server.js
@@ -3,6 +3,7 @@ import session from 'express-session';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
import pkg from './package.json' with { type: "json" }
|
||||
dotenv.config();
|
||||
|
||||
import { initMongo } from './db/mongo.js';
|
||||
@@ -36,9 +37,10 @@ await initMongo();
|
||||
|
||||
// Login-Middleware
|
||||
function requireLogin(req, res, next) {
|
||||
// if (req.session.userId) return next();
|
||||
// res.redirect('/login');
|
||||
return next();
|
||||
if (req.session.userId) {
|
||||
return next();
|
||||
}
|
||||
res.redirect('/login');
|
||||
}
|
||||
|
||||
// Routen registrieren
|
||||
@@ -47,6 +49,11 @@ registerApiRoutes(app, requireLogin);
|
||||
registerAddressRoute(app, requireLogin);
|
||||
|
||||
// Hauptseite
|
||||
app.get('/', requireLogin, (req, res) => res.render('index'));
|
||||
app.get('/', requireLogin, (req, res) => {
|
||||
const version = pkg.version
|
||||
const vdate = pkg.date
|
||||
const isAdmin = req.session && req.session.isAdmin;
|
||||
res.render('index', { isAdmin, version, vdate });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`Server läuft auf http://localhost:${PORT}`));
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('Server.js API', () => {
|
||||
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) };
|
||||
const doc = { espId, sensorNumber, name, description, address, lastUpdatedAt: new Date(), _id: String(entries.length + 1) };
|
||||
entries.push(doc);
|
||||
res.json({ success: true });
|
||||
});
|
||||
@@ -115,7 +115,7 @@ describe('Server.js API', () => {
|
||||
});
|
||||
|
||||
test('PUT /api/update/:id updates entry', async () => {
|
||||
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
|
||||
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: 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');
|
||||
@@ -127,20 +127,20 @@ describe('Server.js API', () => {
|
||||
});
|
||||
|
||||
test('GET /api/list returns all entries', async () => {
|
||||
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
|
||||
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: 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() });
|
||||
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: 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() });
|
||||
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
|
||||
const res = await request(app).delete('/api/delete/1');
|
||||
expect(res.body).toHaveProperty('success', true);
|
||||
expect(entries.length).toBe(0);
|
||||
|
||||
@@ -11,9 +11,11 @@ html(lang="de")
|
||||
div.tabs
|
||||
button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe
|
||||
button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste
|
||||
if isAdmin
|
||||
button.tab-btn#tabUser(type="button" onclick="showTab('user')") User
|
||||
|
||||
// Eingabe-Tab
|
||||
div#tabInputContent.tab-content
|
||||
// Eingabe-Tab
|
||||
div#tabInputContent.tab-content
|
||||
div.card
|
||||
form#entryForm
|
||||
label(for="sensorNumber") Sensornummer:
|
||||
@@ -29,36 +31,60 @@ html(lang="de")
|
||||
textarea#description
|
||||
|
||||
label(for="address") Anschrift:
|
||||
input#address(type="text" placeholder="Wird automatisch ausgefüllt" readonly)
|
||||
input#address(type="text" placeholder="Wird automatisch ausgefüllt" readonly disabled)
|
||||
|
||||
button#saveBtn(type="button") Speichern
|
||||
.twobuttons
|
||||
button#saveBtn(type="button") Speichern
|
||||
button#cancelBtn(type="button") Abbrechen
|
||||
div#result
|
||||
#version Version: #{version} vom #{vdate}
|
||||
|
||||
// Listen-Tab
|
||||
div#tabListContent.tab-content(style="display:none")
|
||||
// Listen-Tab
|
||||
div#tabListContent.tab-content(style="display:none")
|
||||
div.controls
|
||||
button#refreshBtn Aktualisieren
|
||||
| Seite:
|
||||
input#page(value="1")
|
||||
| Limit:
|
||||
input#limit(value="10")
|
||||
input#limit(value="50")
|
||||
span#gzahl
|
||||
|
||||
table#entriesTable
|
||||
colgroup
|
||||
col.col-sensornumber
|
||||
col.col-espid
|
||||
col.col-bezeichnung
|
||||
col.col-beschreibung
|
||||
col.col-date
|
||||
col.col-aktionen
|
||||
thead
|
||||
tr
|
||||
th SensorNr
|
||||
th ESP-ID
|
||||
th Bezeichnung
|
||||
th Beschreibung
|
||||
th Anschrift
|
||||
th Datum
|
||||
th Aktionen
|
||||
th(id="thSensorNr" data-sort="sensorNr" style="cursor:pointer") SensorNr <span id="sortArrowSensorNr">↑</span>
|
||||
th(id="thEspId" data-sort="espId" style="cursor:pointer") ESP-ID <span id="sortArrowEspId">↑</span>
|
||||
th Bezeichnung
|
||||
th Beschreibung
|
||||
th(id="thDate" data-sort="date" style="cursor:pointer") Datum <span id="sortArrowDate">↑</span>
|
||||
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');
|
||||
}
|
||||
|
||||
// User-Tab (nur für Admins)
|
||||
if isAdmin
|
||||
div#tabUserContent.tab-content(style="display:none")
|
||||
div.card
|
||||
h2 Neuen User anlegen
|
||||
form#userForm
|
||||
label(for="username") Benutzername:
|
||||
input#username(type="text" required)
|
||||
label(for="password") Passwort:
|
||||
input#password(type="password" required)
|
||||
label(for="role") Rolle:
|
||||
select#role
|
||||
option(value="user") User
|
||||
option(value="admin") Admin
|
||||
.twobuttons
|
||||
button#userSaveBtn(type="button") Anlegen
|
||||
button#userCancelBtn(type="button") Abbrechen
|
||||
div#userResult
|
||||
#version Version: #{version} vom #{vdate}
|
||||
|
||||
script(type="module" src="/global.js")
|
||||
@@ -6,14 +6,18 @@ html(lang="de")
|
||||
title Login
|
||||
link(rel="stylesheet", href="/styles.css")
|
||||
body
|
||||
h1 Login
|
||||
form(method="POST" action="/login")
|
||||
label(for="email") E-Mail:
|
||||
input#email(type="email" name="email" required)
|
||||
span#emailStatus
|
||||
label(for="password") Passwort:
|
||||
input#password(type="password" name="password" required)
|
||||
button(type="submit") Login
|
||||
if error
|
||||
p.error= error
|
||||
h1 ESP-ID → Sensornummer
|
||||
div.card
|
||||
h2 Login
|
||||
form(method="POST" action="/login")
|
||||
label(for="email") E-Mail:
|
||||
input#email(type="email" name="email" required)
|
||||
span#emailStatus
|
||||
label(for="password") Passwort:
|
||||
input#password(type="password" name="password" required)
|
||||
button(type="submit") Login
|
||||
#version Version: #{version} vom #{vdate}
|
||||
|
||||
if error
|
||||
p.error= error
|
||||
script(type="module" src="/login.js")
|
||||
@@ -1,19 +0,0 @@
|
||||
doctype html
|
||||
html(lang="de")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1")
|
||||
title Registrieren
|
||||
link(rel="stylesheet", href="/styles.css")
|
||||
body
|
||||
h1 Registrierung
|
||||
form(method="POST" action="/register")
|
||||
label(for="email") E-Mail:
|
||||
input#email(type="email" name="email" required)
|
||||
span#emailStatus
|
||||
label(for="password") Passwort:
|
||||
input#password(type="password" name="password" required)
|
||||
button(type="submit") Registrieren
|
||||
if error
|
||||
p.error= error
|
||||
script(type="module" src="/register.js")
|
||||
Reference in New Issue
Block a user