diff --git a/api/Dockerfile b/api/Dockerfile index a9a8f5a..8415648 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -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 . diff --git a/api/data.json b/api/data.json index 0e3657f..c93f562 100644 --- a/api/data.json +++ b/api/data.json @@ -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" } \ No newline at end of file diff --git a/api/server copy.js b/api/server copy.js new file mode 100644 index 0000000..8841e87 --- /dev/null +++ b/api/server copy.js @@ -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}`); +}); diff --git a/api/server.js b/api/server.js index 8e430ba..5abe4b5 100644 --- a/api/server.js +++ b/api/server.js @@ -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}`); +}); \ No newline at end of file diff --git a/docker-compose copy.yml b/docker-compose copy.yml new file mode 100644 index 0000000..6bf9b8d --- /dev/null +++ b/docker-compose copy.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 6bf9b8d..4e511ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/site/index copy.html b/site/index copy.html new file mode 100644 index 0000000..81fa69d --- /dev/null +++ b/site/index copy.html @@ -0,0 +1,247 @@ + + + + + + Sensor Kualitas Udara + + + + + +

🌱 Sensor Kualitas Udara

+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/site/index.html b/site/index.html index 605e330..53f6ef9 100644 --- a/site/index.html +++ b/site/index.html @@ -1,5 +1,6 @@ + Sensor Kualitas Udara @@ -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; + } +

🌱 Sensor Kualitas Udara

-
CO₂: -
-
CO: -
-
BZ: -
-
AQ: -
+
+ +
+ + + + - + + \ No newline at end of file