added phpmyadmin and mysql database, change the html so dont flickering and load container only once
This commit is contained in:
@ -2,7 +2,7 @@ FROM node:20-alpine
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN npm init -y && npm install express
|
RUN npm init -y && npm install express mysql2
|
||||||
|
|
||||||
COPY server.js .
|
COPY server.js .
|
||||||
COPY data.json .
|
COPY data.json .
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"CO2": 2.43,
|
"CO2": 2.78,
|
||||||
"CO": 0.56,
|
"CO": 0.62,
|
||||||
"BZ": 3.11,
|
"BZ": 3.7,
|
||||||
"AQ": 100,
|
"AQ": 100,
|
||||||
"updated_at": "2026-01-30T03:48:16.033Z"
|
"updated_at": "2026-03-11T12:46:55.233Z"
|
||||||
}
|
}
|
||||||
98
api/server copy.js
Normal file
98
api/server copy.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
const DATA_FILE = "./data.json";
|
||||||
|
|
||||||
|
// Helper: read data
|
||||||
|
function readData() {
|
||||||
|
return JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: write data
|
||||||
|
function writeData(data) {
|
||||||
|
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// GET /api/data → read values
|
||||||
|
// ============================
|
||||||
|
app.get("/data", (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = readData();
|
||||||
|
res.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: "Gagal membaca data" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// ADD SENSOR
|
||||||
|
// ============================
|
||||||
|
app.post("/addSensor", (req, res) => {
|
||||||
|
const { id } = req.body;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Sensor ID is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `INSERT INTO sensor_data (id) VALUES (?)`;
|
||||||
|
|
||||||
|
db.query(sql, [id], (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
// handle duplicate ID (optional but nice)
|
||||||
|
if (err.code === "ER_DUP_ENTRY") {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Sensor already exists"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Sensor added successfully",
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// GET /api/update?co2=&co=&bz=&aq=
|
||||||
|
// ===========================================
|
||||||
|
app.get("/update", (req, res) => {
|
||||||
|
const { co2, co, bz, aq } = req.query;
|
||||||
|
|
||||||
|
if (!co2 || !co || !bz || !aq) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Parameter wajib: co2, co, bz, aq"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newData = {
|
||||||
|
CO2: Number(co2),
|
||||||
|
CO: Number(co),
|
||||||
|
BZ: Number(bz),
|
||||||
|
AQ: Number(aq),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeData(newData);
|
||||||
|
res.json({
|
||||||
|
message: "Data berhasil diperbarui",
|
||||||
|
data: newData
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: "Gagal menyimpan data" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`API berjalan di port ${PORT}`);
|
||||||
|
});
|
||||||
327
api/server.js
327
api/server.js
@ -1,63 +1,306 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const fs = require("fs");
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
const DATA_FILE = "./data.json";
|
app.use(express.json());
|
||||||
|
|
||||||
// Helper: read data
|
|
||||||
function readData() {
|
|
||||||
return JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: write data
|
|
||||||
function writeData(data) {
|
|
||||||
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// GET /api/data → read values
|
// DB CONFIG
|
||||||
|
// ============================
|
||||||
|
const dbConfig = {
|
||||||
|
host: "db", // docker service name
|
||||||
|
user: "leman",
|
||||||
|
password: "leman123",
|
||||||
|
database: "air_quality"
|
||||||
|
};
|
||||||
|
|
||||||
|
let db;
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// CONNECT WITH RETRY
|
||||||
|
// ============================
|
||||||
|
function connectDB() {
|
||||||
|
db = mysql.createConnection(dbConfig);
|
||||||
|
|
||||||
|
db.connect(err => {
|
||||||
|
if (err) {
|
||||||
|
console.error("❌ DB connection failed:", err.message);
|
||||||
|
console.log("🔄 Retrying in 5 seconds...");
|
||||||
|
setTimeout(connectDB, 5000);
|
||||||
|
} else {
|
||||||
|
console.log("✅ Connected to MariaDB");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.on("error", err => {
|
||||||
|
console.error("⚠️ DB error:", err.message);
|
||||||
|
|
||||||
|
if (err.code === "PROTOCOL_CONNECTION_LOST") {
|
||||||
|
console.log("🔄 Reconnecting...");
|
||||||
|
connectDB();
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
connectDB();
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// ROOT CHECK
|
||||||
|
// ============================
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.send("API is running 🚀");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// GET LATEST DATA
|
||||||
// ============================
|
// ============================
|
||||||
app.get("/data", (req, res) => {
|
app.get("/data", (req, res) => {
|
||||||
try {
|
const sql = `
|
||||||
const data = readData();
|
SELECT * FROM air_data
|
||||||
res.json(data);
|
ORDER BY updated_at DESC
|
||||||
} catch (err) {
|
LIMIT 1
|
||||||
res.status(500).json({ error: "Gagal membaca data" });
|
`;
|
||||||
}
|
|
||||||
|
db.query(sql, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(results[0] || {});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===========================================
|
// ============================
|
||||||
// GET /api/update?co2=&co=&bz=&aq=
|
// INSERT DATA
|
||||||
// ===========================================
|
// ============================
|
||||||
app.get("/update", (req, res) => {
|
app.get("/update", (req, res) => {
|
||||||
const { co2, co, bz, aq } = req.query;
|
const { co2, co, bz, aq, id } = req.query;
|
||||||
|
|
||||||
if (!co2 || !co || !bz || !aq) {
|
if (!co2 || !co || !bz || !aq || !id) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: "Parameter wajib: co2, co, bz, aq"
|
error: "Parameter wajib: co2, co, bz, aq, id"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newData = {
|
const sql = `
|
||||||
CO2: Number(co2),
|
INSERT INTO air_data (co2, co, bz, aq, id)
|
||||||
CO: Number(co),
|
VALUES (?, ?, ?, ?, ?)
|
||||||
BZ: Number(bz),
|
`;
|
||||||
AQ: Number(aq),
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
db.query(
|
||||||
writeData(newData);
|
sql,
|
||||||
res.json({
|
[
|
||||||
message: "Data berhasil diperbarui",
|
Number(co2),
|
||||||
data: newData
|
Number(co),
|
||||||
});
|
Number(bz),
|
||||||
} catch (err) {
|
Number(aq),
|
||||||
res.status(500).json({ error: "Gagal menyimpan data" });
|
String(id) // sensor id
|
||||||
}
|
],
|
||||||
|
(err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Data berhasil disimpan",
|
||||||
|
insertId: result.insertId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/sensordata/:id", (req, res) => {
|
||||||
|
const sensorId = req.params.id;
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
air_data.co2,
|
||||||
|
air_data.co,
|
||||||
|
air_data.bz,
|
||||||
|
air_data.aq,
|
||||||
|
air_data.updated_at,
|
||||||
|
|
||||||
|
sensor_data.alamat,
|
||||||
|
|
||||||
|
kelurahan.nama AS kelurahan,
|
||||||
|
kecamatan.nama AS kecamatan
|
||||||
|
|
||||||
|
FROM air_data
|
||||||
|
|
||||||
|
JOIN sensor_data
|
||||||
|
ON air_data.id = sensor_data.id
|
||||||
|
|
||||||
|
JOIN kelurahan
|
||||||
|
ON sensor_data.kelurahan = kelurahan.id
|
||||||
|
|
||||||
|
JOIN kecamatan
|
||||||
|
ON kelurahan.kecamatan_id = kecamatan.id
|
||||||
|
|
||||||
|
WHERE air_data.id = ?
|
||||||
|
ORDER BY air_data.updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.query(sql, [sensorId], (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "Sensor not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(results[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/sensors", (req, res) => {
|
||||||
|
const sql = `SELECT id FROM sensor_data`;
|
||||||
|
|
||||||
|
db.query(sql, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// ADD SENSOR
|
||||||
|
// ============================
|
||||||
|
app.post("/addSensor", (req, res) => {
|
||||||
|
const { kelurahan, alamat } = req.body;
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// VALIDATION
|
||||||
|
// ============================
|
||||||
|
if (!kelurahan || !alamat) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "kelurahan dan alamat wajib diisi"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// VALIDATE KELURAHAN EXISTS
|
||||||
|
// ============================
|
||||||
|
const checkSql = `SELECT id FROM kelurahan WHERE id = ?`;
|
||||||
|
|
||||||
|
db.query(checkSql, [kelurahan], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Kelurahan tidak valid"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// INSERT SENSOR
|
||||||
|
// ============================
|
||||||
|
const insertSql = `
|
||||||
|
INSERT INTO sensor_data (kelurahan, alamat)
|
||||||
|
VALUES (?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.query(insertSql, [kelurahan, alamat], (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "DB error saat insert"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Sensor berhasil ditambahkan",
|
||||||
|
sensor_id: result.insertId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// GET ALL DATA (OPTIONAL)
|
||||||
|
// ============================
|
||||||
|
app.get("/history", (req, res) => {
|
||||||
|
const sql = `
|
||||||
|
SELECT * FROM air_data
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.query(sql, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// get kecamatan
|
||||||
|
app.get("/kecamatan", (req, res) => {
|
||||||
|
const sql = `SELECT id, nama FROM kecamatan ORDER BY nama ASC`;
|
||||||
|
|
||||||
|
db.query(sql, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// get kelurahan by kecamatan_id
|
||||||
|
app.get("/kelurahan", (req, res) => {
|
||||||
|
const { kecamatan_id } = req.query;
|
||||||
|
|
||||||
|
let sql = `
|
||||||
|
SELECT
|
||||||
|
kelurahan.id,
|
||||||
|
kelurahan.nama,
|
||||||
|
kecamatan.nama AS kecamatan
|
||||||
|
FROM kelurahan
|
||||||
|
JOIN kecamatan ON kelurahan.kecamatan_id = kecamatan.id
|
||||||
|
`;
|
||||||
|
|
||||||
|
let params = [];
|
||||||
|
|
||||||
|
if (kecamatan_id) {
|
||||||
|
sql += ` WHERE kelurahan.kecamatan_id = ?`;
|
||||||
|
params.push(kecamatan_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ` ORDER BY kelurahan.nama ASC`;
|
||||||
|
|
||||||
|
db.query(sql, params, (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: "DB error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// START SERVER
|
||||||
|
// ============================
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`API berjalan di port ${PORT}`);
|
console.log(`🚀 API running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
21
docker-compose copy.yml
Normal file
21
docker-compose copy.yml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: air-quality-web
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./site:/usr/share/nginx/html:ro
|
||||||
|
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: ./api
|
||||||
|
container_name: air-quality-api
|
||||||
|
volumes:
|
||||||
|
- ./api/data.json:/app/data.json
|
||||||
|
restart: unless-stopped
|
||||||
@ -15,7 +15,36 @@ services:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
build: ./api
|
build: ./api
|
||||||
|
# ports:
|
||||||
|
# - "3000:3000"
|
||||||
container_name: air-quality-api
|
container_name: air-quality-api
|
||||||
volumes:
|
depends_on:
|
||||||
- ./api/data.json:/app/data.json
|
- db # 👈 IMPORTANT
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mariadb:10.5
|
||||||
|
container_name: air-quality-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: leman123
|
||||||
|
MYSQL_DATABASE: air_quality
|
||||||
|
MYSQL_USER: leman
|
||||||
|
MYSQL_PASSWORD: leman123
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin/phpmyadmin
|
||||||
|
container_name: air-quality-pma
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
environment:
|
||||||
|
PMA_HOST: db
|
||||||
|
PMA_PORT: 3306
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
247
site/index copy.html
Normal file
247
site/index copy.html
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Sensor Kualitas Udara</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #1e293b;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>🌱 Sensor Kualitas Udara</h1>
|
||||||
|
|
||||||
|
<div id="container"></div>
|
||||||
|
<hr>
|
||||||
|
<button onclick="openDialog()">Add Sensor</button>
|
||||||
|
<div id="dialog"
|
||||||
|
style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7);">
|
||||||
|
<div style="background:#1e293b; padding:20px; width:300px; margin:100px auto; border-radius:10px;">
|
||||||
|
|
||||||
|
<h3>Tambah Sensor</h3>
|
||||||
|
|
||||||
|
<label>Kecamatan:</label><br>
|
||||||
|
<select id="kecamatan" onchange="loadKelurahan()">
|
||||||
|
<option value="">-- Pilih Kecamatan --</option>
|
||||||
|
</select>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<label>Kelurahan:</label><br>
|
||||||
|
<select id="kelurahan">
|
||||||
|
<option value="">-- Pilih Kelurahan --</option>
|
||||||
|
</select>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<label>Alamat:</label><br>
|
||||||
|
<textarea id="alamat" rows="3" style="width:100%"></textarea>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<button onclick="addSensor()">Simpan</button>
|
||||||
|
<button onclick="closeDialog()">Batal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getStatus(aq) {
|
||||||
|
if (aq >= 90) return "Sangat Baik";
|
||||||
|
if (aq >= 70) return "Baik";
|
||||||
|
if (aq >= 50) return "Sedang";
|
||||||
|
if (aq > 30) return "Buruk";
|
||||||
|
return "Sangat Buruk";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAllSensors() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/sensors");
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch sensors", err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSensorData(id) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/sensordata/${id}`);
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed sensor ${id}`, err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
const container = document.getElementById("container");
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
const sensors = await fetchAllSensors();
|
||||||
|
|
||||||
|
for (const sensor of sensors) {
|
||||||
|
const data = await fetchSensorData(sensor.id);
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "card";
|
||||||
|
|
||||||
|
if (!data || data.error) {
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>Sensor ID: ${sensor.id}</h3>
|
||||||
|
<p style="color: orange;">Belum Ada Data</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>Sensor ID: ${sensor.id}</h3>
|
||||||
|
<small>${data.kecamatan} - ${data.kelurahan}</small><br>
|
||||||
|
<small>${data.alamat}</small><br><br>
|
||||||
|
|
||||||
|
Karbon Oksida: ${data.co2} ppm<br>
|
||||||
|
Karbon Monoksida: ${data.co} ppm<br>
|
||||||
|
Benzena: ${data.bz} ppm<br>
|
||||||
|
Jumlah: ${data.aq} %<br>
|
||||||
|
Kualitas: ${getStatus(data.aq)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// ADD SENSOR
|
||||||
|
// ============================
|
||||||
|
async function addSensor() {
|
||||||
|
const kecamatan = document.getElementById("kecamatan").value;
|
||||||
|
const kelurahan = document.getElementById("kelurahan").value;
|
||||||
|
const alamat = document.getElementById("alamat").value;
|
||||||
|
|
||||||
|
if (!kecamatan || !kelurahan || !alamat) {
|
||||||
|
alert("Semua field wajib diisi!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/addSensor", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
kecamatan,
|
||||||
|
kelurahan,
|
||||||
|
alamat
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
alert(result.error || "Gagal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Sensor berhasil ditambahkan!");
|
||||||
|
|
||||||
|
closeDialog(); // 👈 close popup
|
||||||
|
|
||||||
|
// reset form
|
||||||
|
document.getElementById("kecamatan").value = "";
|
||||||
|
document.getElementById("kelurahan").innerHTML = `<option value="">-- Pilih Kelurahan --</option>`;
|
||||||
|
document.getElementById("alamat").value = "";
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert("Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
setInterval(fetchData, 5000);
|
||||||
|
|
||||||
|
function openDialog() {
|
||||||
|
document.getElementById("dialog").style.display = "block";
|
||||||
|
loadKecamatan(); // load when opened
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
document.getElementById("dialog").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// load data kecamatan
|
||||||
|
async function loadKecamatan() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/kecamatan");
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const select = document.getElementById("kecamatan");
|
||||||
|
|
||||||
|
// reset
|
||||||
|
select.innerHTML = `<option value="">-- Pilih Kecamatan --</option>`;
|
||||||
|
|
||||||
|
data.forEach(k => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = k.id;
|
||||||
|
option.textContent = k.nama;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed load kecamatan", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// load data kelurahan
|
||||||
|
async function loadKelurahan() {
|
||||||
|
const kecamatanId = document.getElementById("kecamatan").value;
|
||||||
|
|
||||||
|
const select = document.getElementById("kelurahan");
|
||||||
|
|
||||||
|
// reset first
|
||||||
|
select.innerHTML = `<option value="">-- Pilih Kelurahan --</option>`;
|
||||||
|
|
||||||
|
if (!kecamatanId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/kelurahan?kecamatan_id=${kecamatanId}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
data.forEach(k => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = k.id;
|
||||||
|
option.textContent = k.nama;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed load kelurahan", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
251
site/index.html
251
site/index.html
@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Sensor Kualitas Udara</title>
|
<title>Sensor Kualitas Udara</title>
|
||||||
@ -10,6 +11,7 @@
|
|||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@ -17,35 +19,254 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>🌱 Sensor Kualitas Udara</h1>
|
<h1>🌱 Sensor Kualitas Udara</h1>
|
||||||
|
|
||||||
<div class="card">CO₂: <span id="co2">-</span></div>
|
<div id="container"></div>
|
||||||
<div class="card">CO: <span id="co">-</span></div>
|
|
||||||
<div class="card">BZ: <span id="bz">-</span></div>
|
<hr>
|
||||||
<div class="card">AQ: <span id="aq">-</span></div>
|
<button onclick="openDialog()">Add Sensor</button>
|
||||||
|
|
||||||
|
<!-- MODAL -->
|
||||||
|
<div id="dialog"
|
||||||
|
style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7);">
|
||||||
|
<div style="background:#1e293b; padding:20px; width:300px; margin:100px auto; border-radius:10px;">
|
||||||
|
|
||||||
|
<h3>Tambah Sensor</h3>
|
||||||
|
|
||||||
|
<label>Kecamatan:</label><br>
|
||||||
|
<select id="kecamatan" onchange="loadKelurahan()">
|
||||||
|
<option value="">-- Pilih Kecamatan --</option>
|
||||||
|
</select>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<label>Kelurahan:</label><br>
|
||||||
|
<select id="kelurahan">
|
||||||
|
<option value="">-- Pilih Kelurahan --</option>
|
||||||
|
</select>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<label>Alamat:</label><br>
|
||||||
|
<textarea id="alamat" rows="3" style="width:100%"></textarea>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<button onclick="addSensor()">Simpan</button>
|
||||||
|
<button onclick="closeDialog()">Batal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function fetchData() {
|
function getStatus(aq) {
|
||||||
try {
|
if (aq >= 90) return "Sangat Baik";
|
||||||
const res = await fetch("/api/data");
|
if (aq >= 70) return "Baik";
|
||||||
const data = await res.json();
|
if (aq >= 50) return "Sedang";
|
||||||
|
if (aq > 30) return "Buruk";
|
||||||
|
return "Sangat Buruk";
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("co2").textContent = data.CO2 + 'ppm';
|
async function fetchAllSensors() {
|
||||||
document.getElementById("co").textContent = data.CO + 'ppm';
|
try {
|
||||||
document.getElementById("bz").textContent = data.BZ + 'ppm';
|
const res = await fetch("/api/sensors");
|
||||||
document.getElementById("aq").textContent = data.AQ + '%';
|
return await res.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch data", err);
|
console.error("Failed to fetch sensors", err);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchData();
|
async function fetchSensorData(id) {
|
||||||
setInterval(fetchData, 3000); // refresh every 3 seconds
|
try {
|
||||||
|
const res = await fetch(`/api/sensordata/${id}`);
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed sensor ${id}`, err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// BUILD UI ONCE
|
||||||
|
// ============================
|
||||||
|
async function initSensors() {
|
||||||
|
const container = document.getElementById("container");
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
const sensors = await fetchAllSensors();
|
||||||
|
|
||||||
|
for (const sensor of sensors) {
|
||||||
|
const data = await fetchSensorData(sensor.id);
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "card";
|
||||||
|
|
||||||
|
if (!data || data.error) {
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>Sensor ID: ${sensor.id}</h3>
|
||||||
|
<p style="color: orange;">Belum Ada Data</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>Sensor ID: ${sensor.id}</h3>
|
||||||
|
<small>${data.kecamatan} - ${data.kelurahan}</small><br>
|
||||||
|
<small>${data.alamat}</small><br><br>
|
||||||
|
|
||||||
|
Karbon Oksida: <span id="co2-${sensor.id}">-</span> ppm<br>
|
||||||
|
Karbon Monoksida: <span id="co-${sensor.id}">-</span> ppm<br>
|
||||||
|
Benzena: <span id="bz-${sensor.id}">-</span> ppm<br>
|
||||||
|
Jumlah: <span id="aq-${sensor.id}">-</span> %<br>
|
||||||
|
Kualitas: <span id="qa-${sensor.id}">-</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// UPDATE VALUES ONLY
|
||||||
|
// ============================
|
||||||
|
async function updateSensorValues() {
|
||||||
|
const sensors = await fetchAllSensors();
|
||||||
|
|
||||||
|
for (const sensor of sensors) {
|
||||||
|
const data = await fetchSensorData(sensor.id);
|
||||||
|
|
||||||
|
if (!data || data.error) continue;
|
||||||
|
|
||||||
|
document.getElementById(`co2-${sensor.id}`).textContent = data.co2;
|
||||||
|
document.getElementById(`co-${sensor.id}`).textContent = data.co;
|
||||||
|
document.getElementById(`bz-${sensor.id}`).textContent = data.bz;
|
||||||
|
document.getElementById(`aq-${sensor.id}`).textContent = data.aq;
|
||||||
|
document.getElementById(`qa-${sensor.id}`).textContent = getStatus(data.aq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// MODAL CONTROL
|
||||||
|
// ============================
|
||||||
|
function openDialog() {
|
||||||
|
document.getElementById("dialog").style.display = "block";
|
||||||
|
loadKecamatan();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
document.getElementById("dialog").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// LOAD KECAMATAN
|
||||||
|
// ============================
|
||||||
|
async function loadKecamatan() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/kecamatan");
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const select = document.getElementById("kecamatan");
|
||||||
|
select.innerHTML = `<option value="">-- Pilih Kecamatan --</option>`;
|
||||||
|
|
||||||
|
data.forEach(k => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = k.id;
|
||||||
|
option.textContent = k.nama;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed load kecamatan", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// LOAD KELURAHAN
|
||||||
|
// ============================
|
||||||
|
async function loadKelurahan() {
|
||||||
|
const kecamatanId = document.getElementById("kecamatan").value;
|
||||||
|
|
||||||
|
const select = document.getElementById("kelurahan");
|
||||||
|
select.innerHTML = `<option value="">-- Pilih Kelurahan --</option>`;
|
||||||
|
|
||||||
|
if (!kecamatanId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/kelurahan?kecamatan_id=${kecamatanId}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
data.forEach(k => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = k.id;
|
||||||
|
option.textContent = k.nama;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed load kelurahan", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// ADD SENSOR
|
||||||
|
// ============================
|
||||||
|
async function addSensor() {
|
||||||
|
const kelurahan = document.getElementById("kelurahan").value;
|
||||||
|
const alamat = document.getElementById("alamat").value;
|
||||||
|
|
||||||
|
if (!kelurahan || !alamat) {
|
||||||
|
alert("Semua field wajib diisi!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/addSensor", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
kelurahan,
|
||||||
|
alamat
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
alert(result.error || "Gagal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Sensor berhasil ditambahkan!");
|
||||||
|
|
||||||
|
closeDialog();
|
||||||
|
|
||||||
|
document.getElementById("kecamatan").value = "";
|
||||||
|
document.getElementById("kelurahan").innerHTML = `<option value="">-- Pilih Kelurahan --</option>`;
|
||||||
|
document.getElementById("alamat").value = "";
|
||||||
|
|
||||||
|
await initSensors(); // rebuild only when new sensor added
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert("Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// INIT
|
||||||
|
// ============================
|
||||||
|
initSensors();
|
||||||
|
updateSensorValues()
|
||||||
|
setInterval(updateSensorValues, 5000);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user