added phpmyadmin and mysql database, change the html so dont flickering and load container only once

This commit is contained in:
cowrie
2026-04-15 22:50:07 +08:00
parent a2f9e06d09
commit 4530afde5f
8 changed files with 925 additions and 66 deletions

View File

@ -2,7 +2,7 @@ FROM node:20-alpine
WORKDIR /app
RUN npm init -y && npm install express
RUN npm init -y && npm install express mysql2
COPY server.js .
COPY data.json .

View File

@ -1,7 +1,7 @@
{
"CO2": 2.43,
"CO": 0.56,
"BZ": 3.11,
"CO2": 2.78,
"CO": 0.62,
"BZ": 3.7,
"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
View 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}`);
});

View File

@ -1,63 +1,306 @@
const express = require("express");
const fs = require("fs");
const mysql = require("mysql2");
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));
}
app.use(express.json());
// ============================
// 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) => {
try {
const data = readData();
res.json(data);
} catch (err) {
res.status(500).json({ error: "Gagal membaca data" });
}
const sql = `
SELECT * FROM air_data
ORDER BY updated_at DESC
LIMIT 1
`;
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) => {
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({
error: "Parameter wajib: co2, co, bz, aq"
error: "Parameter wajib: co2, co, bz, aq, id"
});
}
const newData = {
CO2: Number(co2),
CO: Number(co),
BZ: Number(bz),
AQ: Number(aq),
updated_at: new Date().toISOString()
};
const sql = `
INSERT INTO air_data (co2, co, bz, aq, id)
VALUES (?, ?, ?, ?, ?)
`;
try {
writeData(newData);
res.json({
message: "Data berhasil diperbarui",
data: newData
});
} catch (err) {
res.status(500).json({ error: "Gagal menyimpan data" });
}
db.query(
sql,
[
Number(co2),
Number(co),
Number(bz),
Number(aq),
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, () => {
console.log(`API berjalan di port ${PORT}`);
});
console.log(`🚀 API running on port ${PORT}`);
});

21
docker-compose copy.yml Normal file
View 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

View File

@ -15,7 +15,36 @@ services:
api:
build: ./api
# ports:
# - "3000:3000"
container_name: air-quality-api
volumes:
- ./api/data.json:/app/data.json
depends_on:
- db # 👈 IMPORTANT
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
View 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>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sensor Kualitas Udara</title>
@ -10,6 +11,7 @@
color: #e5e7eb;
padding: 20px;
}
.card {
background: #1e293b;
padding: 15px;
@ -17,35 +19,254 @@
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 class="card">CO₂: <span id="co2">-</span></div>
<div class="card">CO: <span id="co">-</span></div>
<div class="card">BZ: <span id="bz">-</span></div>
<div class="card">AQ: <span id="aq">-</span></div>
<div id="container"></div>
<hr>
<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>
async function fetchData() {
try {
const res = await fetch("/api/data");
const data = await res.json();
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";
}
document.getElementById("co2").textContent = data.CO2 + 'ppm';
document.getElementById("co").textContent = data.CO + 'ppm';
document.getElementById("bz").textContent = data.BZ + 'ppm';
document.getElementById("aq").textContent = data.AQ + '%';
async function fetchAllSensors() {
try {
const res = await fetch("/api/sensors");
return await res.json();
} catch (err) {
console.error("Failed to fetch data", err);
console.error("Failed to fetch sensors", err);
return [];
}
}
fetchData();
setInterval(fetchData, 3000); // refresh every 3 seconds
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;
}
}
// ============================
// 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>
</body>
</html>
</html>