First commit - V 1.0.0
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
18
.vscode/launch.json
vendored
Normal file
18
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Program",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/server.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Node.js 20 LTS als Basis
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# Arbeitsverzeichnis
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# package.json + package-lock.json kopieren
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Dependencies installieren
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Restliche App-Dateien kopieren
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Port in Container
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Startkommando
|
||||||
|
# CMD ["npm", "run", "dev"]
|
||||||
|
CMD ["npm", "start"]
|
||||||
28
docker-compose.yml
Normal file
28
docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: esp-app
|
||||||
|
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
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo:6
|
||||||
|
container_name: esp-mongo
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
1391
package-lock.json
generated
Normal file
1391
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "espid2sensor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon --watch server.js --watch views --watch public server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"mongodb": "^6.6.0",
|
||||||
|
"pug": "^3.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
76
public/global.js
Normal file
76
public/global.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
const resultEl = document.getElementById('result');
|
||||||
|
const espIn = document.getElementById('espId');
|
||||||
|
const sensorIn = document.getElementById('sensorNumber');
|
||||||
|
|
||||||
|
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||||
|
const espId = espIn.value.trim();
|
||||||
|
const sensorNumber = sensorIn.value.trim();
|
||||||
|
if (!espId || !sensorNumber) {
|
||||||
|
resultEl.innerText = 'Bitte ESP-ID und Sensornummer eingeben.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resultEl.innerText = 'Speichere...';
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ espId, sensorNumber })
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.ok) {
|
||||||
|
resultEl.innerHTML = `<strong>Gespeichert:</strong> ESP-ID = ${j.entry.espId}, Sensor = ${j.entry.sensorNumber}`;
|
||||||
|
espIn.value = '';
|
||||||
|
sensorIn.value = '';
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
resultEl.innerText = 'Fehler: ' + (j.error || 'Unbekannt');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
resultEl.innerText = 'Netzwerkfehler';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
const page = document.getElementById('page').value || 1;
|
||||||
|
const limit = document.getElementById('limit').value || 25;
|
||||||
|
const listEl = document.getElementById('list');
|
||||||
|
listEl.innerText = 'Lade...';
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/list?page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`);
|
||||||
|
const j = await r.json();
|
||||||
|
if (!j.ok) { listEl.innerText = 'Fehler beim Laden'; return; }
|
||||||
|
if (j.items.length === 0) { listEl.innerText = 'Keine Einträge'; return;}
|
||||||
|
let html = `<div>Ergebnis: ${j.items.length} von ${j.total} (Seite ${j.page})</div>`;
|
||||||
|
html += '<table><thead><tr><th>Datum</th><th>ESP-ID</th><th>Sensor</th><th></th></tr></thead><tbody>';
|
||||||
|
j.items.forEach(it => {
|
||||||
|
html += `<tr>
|
||||||
|
<td>${it.createdAt}</td>
|
||||||
|
<td>${it.espId}</td>
|
||||||
|
<td>${it.sensorNumber}</td>
|
||||||
|
<td><button onclick="deleteEntry('${it._id}')">Löschen</button></td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
listEl.innerHTML = html;
|
||||||
|
} catch {
|
||||||
|
listEl.innerText = 'Netzwerkfehler beim Laden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteEntry(id) {
|
||||||
|
if (!confirm('Diesen Eintrag wirklich löschen?')) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/entry/${id}`, { method: 'DELETE' });
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.ok) {
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Löschen');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
alert('Netzwerkfehler');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('refreshBtn').addEventListener('click', loadList);
|
||||||
|
loadList();
|
||||||
118
public/index.html
Normal file
118
public/index.html
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>ESP-ID + Sensornummer speichern</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, sans-serif; padding: 20px; max-width:800px; margin:auto; }
|
||||||
|
input, button { font-size: 1rem; padding: 8px; }
|
||||||
|
.card { border: 1px solid #ddd; padding: 12px; border-radius: 8px; margin-bottom: 12px; }
|
||||||
|
table { width:100%; border-collapse: collapse; }
|
||||||
|
th, td { text-align:left; padding:8px; border-bottom:1px solid #eee; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>ESP-ID + Sensornummer speichern</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<label>ESP-ID:</label>
|
||||||
|
<input id="espId" placeholder="z.B. esp-1234" />
|
||||||
|
<br><br>
|
||||||
|
<label>Sensornummer:</label>
|
||||||
|
<input id="sensorNumber" placeholder="z.B. 42" />
|
||||||
|
<br><br>
|
||||||
|
<button id="saveBtn">Speichern</button>
|
||||||
|
<div id="result" style="margin-top:10px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Gespeicherte Einträge</h2>
|
||||||
|
<div style="margin-bottom:8px;">
|
||||||
|
<button id="refreshBtn">Aktualisieren</button>
|
||||||
|
Seite: <input id="page" value="1" style="width:50px" />
|
||||||
|
Limit: <input id="limit" value="25" style="width:50px" />
|
||||||
|
</div>
|
||||||
|
<div id="list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const resultEl = document.getElementById('result');
|
||||||
|
const espIn = document.getElementById('espId');
|
||||||
|
const sensorIn = document.getElementById('sensorNumber');
|
||||||
|
|
||||||
|
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||||
|
const espId = espIn.value.trim();
|
||||||
|
const sensorNumber = sensorIn.value.trim();
|
||||||
|
if (!espId || !sensorNumber) {
|
||||||
|
resultEl.innerText = 'Bitte ESP-ID und Sensornummer eingeben.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resultEl.innerText = 'Speichere...';
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ espId, sensorNumber })
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.ok) {
|
||||||
|
resultEl.innerHTML = `<strong>Gespeichert:</strong> ESP-ID = ${j.entry.espId}, Sensor = ${j.entry.sensorNumber}`;
|
||||||
|
espIn.value = '';
|
||||||
|
sensorIn.value = '';
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
resultEl.innerText = 'Fehler: ' + (j.error || 'Unbekannt');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
resultEl.innerText = 'Netzwerkfehler';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
const page = document.getElementById('page').value || 1;
|
||||||
|
const limit = document.getElementById('limit').value || 25;
|
||||||
|
const listEl = document.getElementById('list');
|
||||||
|
listEl.innerText = 'Lade...';
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/list?page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`);
|
||||||
|
const j = await r.json();
|
||||||
|
if (!j.ok) { listEl.innerText = 'Fehler beim Laden'; return; }
|
||||||
|
if (j.items.length === 0) { listEl.innerText = 'Keine Einträge'; return;}
|
||||||
|
let html = `<div>Ergebnis: ${j.items.length} von ${j.total} (Seite ${j.page})</div>`;
|
||||||
|
html += '<table><thead><tr><th>Datum</th><th>ESP-ID</th><th>Sensor</th><th></th></tr></thead><tbody>';
|
||||||
|
j.items.forEach(it => {
|
||||||
|
html += `<tr>
|
||||||
|
<td>${it.createdAt}</td>
|
||||||
|
<td>${it.espId}</td>
|
||||||
|
<td>${it.sensorNumber}</td>
|
||||||
|
<td><button onclick="deleteEntry('${it._id}')">Löschen</button></td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
listEl.innerHTML = html;
|
||||||
|
} catch (err) {
|
||||||
|
listEl.innerText = 'Netzwerkfehler beim Laden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteEntry(id) {
|
||||||
|
if (!confirm('Diesen Eintrag wirklich löschen?')) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/entry/${id}`, { method: 'DELETE' });
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.ok) {
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Löschen');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert('Netzwerkfehler');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('refreshBtn').addEventListener('click', loadList);
|
||||||
|
loadList();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
public/styles.css
Normal file
5
public/styles.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
body { font-family: system-ui, sans-serif; padding: 20px; max-width:800px; margin:auto; }
|
||||||
|
input, button { font-size: 1rem; padding: 8px; margin: 2px; }
|
||||||
|
.card { border: 1px solid #ddd; padding: 12px; border-radius: 8px; margin-bottom: 12px; }
|
||||||
|
table { width:100%; border-collapse: collapse; }
|
||||||
|
th, td { text-align:left; padding:8px; border-bottom:1px solid #eee; }
|
||||||
102
server.js
Normal file
102
server.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const { MongoClient, ObjectId } = require('mongodb');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Statische Dateien (z.B. global.js, CSS) ausliefern
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
// Pug als Template Engine einrichten
|
||||||
|
app.set('view engine', 'pug');
|
||||||
|
app.set('views', './views');
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
|
||||||
|
const DB_NAME = process.env.DB_NAME || 'espdb';
|
||||||
|
|
||||||
|
let db, entriesCollection;
|
||||||
|
|
||||||
|
// MongoDB-Verbindung herstellen
|
||||||
|
async function initMongo() {
|
||||||
|
const client = new MongoClient(MONGO_URI);
|
||||||
|
await client.connect();
|
||||||
|
db = client.db(DB_NAME);
|
||||||
|
entriesCollection = db.collection('entries');
|
||||||
|
console.log(`MongoDB verbunden: ${MONGO_URI}/${DB_NAME}`);
|
||||||
|
}
|
||||||
|
initMongo().catch(err => {
|
||||||
|
console.error('MongoDB Verbindungsfehler:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Web-Seite (Pug) rendern ---
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.render('index');
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- API ---
|
||||||
|
app.post('/api/save', async (req, res) => {
|
||||||
|
const { espId, sensorNumber } = req.body;
|
||||||
|
if (!espId || !sensorNumber) {
|
||||||
|
return res.status(400).json({ ok: false, error: 'ESP-ID und Sensornummer sind erforderlich' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const doc = {
|
||||||
|
espId: String(espId).trim(),
|
||||||
|
sensorNumber: String(sensorNumber).trim(),
|
||||||
|
createdAt: new Date()
|
||||||
|
};
|
||||||
|
const result = await entriesCollection.insertOne(doc);
|
||||||
|
return res.json({ ok: true, id: result.insertedId, entry: doc });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ ok: false, error: 'DB Fehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/list', async (req, res) => {
|
||||||
|
const page = Math.max(1, parseInt(req.query.page) || 1);
|
||||||
|
const limit = Math.max(1, Math.min(100, parseInt(req.query.limit) || 50));
|
||||||
|
try {
|
||||||
|
const total = await entriesCollection.countDocuments();
|
||||||
|
const rawItems = await entriesCollection.find({})
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.skip((page - 1) * limit)
|
||||||
|
.limit(limit)
|
||||||
|
.toArray();
|
||||||
|
const items = rawItems.map(it => ({
|
||||||
|
...it,
|
||||||
|
createdAt: formatDate(it.createdAt)
|
||||||
|
}));
|
||||||
|
return res.json({ ok: true, page, limit, total, items });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ ok: false, error: 'DB Fehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/entry/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await entriesCollection.deleteOne({ _id: new ObjectId(req.params.id) });
|
||||||
|
return res.json({ ok: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ ok: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server läuft auf http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
33
views/index.pug
Normal file
33
views/index.pug
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
doctype html
|
||||||
|
html(lang="de")
|
||||||
|
head
|
||||||
|
meta(charset="utf-8")
|
||||||
|
meta(name="viewport" content="width=device-width,initial-scale=1")
|
||||||
|
title ESP-ID + Sensornummer speichern
|
||||||
|
link(rel="stylesheet", href="/styles.css")
|
||||||
|
body
|
||||||
|
h1 ESP-ID + Sensornummer speichern
|
||||||
|
|
||||||
|
.card
|
||||||
|
label(for="espId") ESP-id:
|
||||||
|
input#espId(placeholder="z.B. esp-1234")
|
||||||
|
br
|
||||||
|
br
|
||||||
|
label(for="sensorNumber") Sensornummer:
|
||||||
|
input#sensorNumber(placeholder="z.B. 42")
|
||||||
|
br
|
||||||
|
br
|
||||||
|
button#saveBtn Speichern
|
||||||
|
div#result(style="margin-top:10px")
|
||||||
|
|
||||||
|
.card
|
||||||
|
h2 Gespeicherte Einträge
|
||||||
|
div(style="margin-bottom:8px;")
|
||||||
|
button#refreshBtn Aktualisieren
|
||||||
|
| Seite:
|
||||||
|
input#page(value="1" style="width:50px")
|
||||||
|
| Limit:
|
||||||
|
input#limit(value="25" style="width:50px")
|
||||||
|
div#list
|
||||||
|
|
||||||
|
script(src="/global.js")
|
||||||
Reference in New Issue
Block a user