From 0efe07bb4543ea79021790d0ce6e788860a33a22 Mon Sep 17 00:00:00 2001 From: Naufal Date: Wed, 8 Jul 2026 22:09:18 +0700 Subject: [PATCH] backend Aplikasi --- backend-Sigasti/.gitignore | 18 + backend-Sigasti/DEPLOYMENT_GUIDE.md | 166 +++ backend-Sigasti/README.md | 59 + backend-Sigasti/config/database.js | 23 + backend-Sigasti/models/sterilisasi.js | 108 ++ backend-Sigasti/mqtt/historyHelper.js | 133 +++ backend-Sigasti/mqtt/mqttClient.js | 321 ++++++ backend-Sigasti/package-lock.json | 1437 +++++++++++++++++++++++++ backend-Sigasti/package.json | 20 + backend-Sigasti/railway.toml | 7 + backend-Sigasti/routes/sterilisasi.js | 433 ++++++++ backend-Sigasti/server.js | 25 + backend-Sigasti/test-batch-id.js | 105 ++ backend-Sigasti/test-history-api.js | 88 ++ 14 files changed, 2943 insertions(+) create mode 100644 backend-Sigasti/.gitignore create mode 100644 backend-Sigasti/DEPLOYMENT_GUIDE.md create mode 100644 backend-Sigasti/README.md create mode 100644 backend-Sigasti/config/database.js create mode 100644 backend-Sigasti/models/sterilisasi.js create mode 100644 backend-Sigasti/mqtt/historyHelper.js create mode 100644 backend-Sigasti/mqtt/mqttClient.js create mode 100644 backend-Sigasti/package-lock.json create mode 100644 backend-Sigasti/package.json create mode 100644 backend-Sigasti/railway.toml create mode 100644 backend-Sigasti/routes/sterilisasi.js create mode 100644 backend-Sigasti/server.js create mode 100644 backend-Sigasti/test-batch-id.js create mode 100644 backend-Sigasti/test-history-api.js diff --git a/backend-Sigasti/.gitignore b/backend-Sigasti/.gitignore new file mode 100644 index 0000000..b934d86 --- /dev/null +++ b/backend-Sigasti/.gitignore @@ -0,0 +1,18 @@ +# Dependencies +node_modules/ + +# Environment variables +.env + +# Logs +logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ diff --git a/backend-Sigasti/DEPLOYMENT_GUIDE.md b/backend-Sigasti/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..2532825 --- /dev/null +++ b/backend-Sigasti/DEPLOYMENT_GUIDE.md @@ -0,0 +1,166 @@ +# Panduan Deploy Backend ke Railway + +## Persiapan + +### 1. Setup MongoDB Atlas (Database Cloud) + +1. Buka [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) +2. Buat akun gratis (jika belum punya) +3. Buat cluster baru (pilih FREE tier) +4. Tunggu cluster selesai dibuat (±5 menit) +5. Klik "Connect" → "Connect your application" +6. Copy connection string, contoh: + ``` + mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/autoclave?retryWrites=true&w=majority + ``` +7. Ganti `` dengan password database Anda +8. Simpan connection string ini untuk nanti + +### 2. Push Code ke GitHub + +#### A. Inisialisasi Git (jika belum) + +```bash +cd Back-end +git init +``` + +#### B. Buat repository di GitHub + +1. Buka [GitHub](https://github.com) +2. Klik tombol "+" → "New repository" +3. Nama repository: `autoclave-backend` (atau nama lain) +4. Pilih "Public" atau "Private" +5. **JANGAN** centang "Initialize with README" (karena sudah ada) +6. Klik "Create repository" + +#### C. Push code ke GitHub + +```bash +# Tambahkan semua file +git add . + +# Commit +git commit -m "Initial commit: Backend autoclave system" + +# Tambahkan remote (ganti dengan URL repository Anda) +git remote add origin https://github.com/username/autoclave-backend.git + +# Push ke GitHub +git branch -M main +git push -u origin main +``` + +### 3. Deploy ke Railway + +#### A. Buat akun Railway + +1. Buka [Railway.app](https://railway.app) +2. Klik "Login" → pilih "Login with GitHub" +3. Authorize Railway untuk akses GitHub Anda + +#### B. Deploy dari GitHub + +1. Di Railway dashboard, klik "New Project" +2. Pilih "Deploy from GitHub repo" +3. Pilih repository `autoclave-backend` yang baru dibuat +4. Railway akan otomatis detect Node.js project +5. Klik "Deploy Now" + +#### C. Set Environment Variables + +1. Setelah deploy, klik project Anda +2. Klik tab "Variables" +3. Tambahkan variabel berikut: + + ``` + PORT=5000 + MONGODB_URI=mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/autoclave?retryWrites=true&w=majority + ``` + +4. Klik "Add" untuk setiap variabel + +#### D. Dapatkan URL Production + +1. Klik tab "Settings" +2. Scroll ke "Domains" +3. Klik "Generate Domain" +4. Copy URL yang digenerate, contoh: + ``` + https://autoclave-backend-production.up.railway.app + ``` + +### 4. Update Frontend + +Setelah mendapat URL production, update file `.env` di frontend: + +```env +BACKEND_URL=https://autoclave-backend-production.up.railway.app +``` + +Atau update `Front-end/src/config.ts`: + +```typescript +export const BACKEND_URL = 'https://autoclave-backend-production.up.railway.app'; +``` + +## Testing + +Test endpoint dengan curl atau Postman: + +```bash +# Test root endpoint +curl https://your-app.up.railway.app/ + +# Test API endpoint +curl https://your-app.up.railway.app/sterilisasi/running/last +``` + +## Monitoring + +1. Di Railway dashboard, klik project Anda +2. Klik tab "Deployments" untuk melihat status deploy +3. Klik tab "Logs" untuk melihat log real-time +4. Klik tab "Metrics" untuk melihat usage + +## Troubleshooting + +### Deploy Gagal + +1. Cek logs di Railway dashboard +2. Pastikan `package.json` memiliki script `"start": "node server.js"` +3. Pastikan semua dependencies sudah di-install + +### Database Connection Error + +1. Pastikan MongoDB Atlas cluster sudah running +2. Pastikan IP whitelist di MongoDB Atlas diset ke `0.0.0.0/0` (allow all) +3. Pastikan connection string benar (username, password, database name) + +### MQTT Connection Error + +1. MQTT broker (HiveMQ) harus bisa diakses dari Railway +2. Cek logs untuk error connection +3. Pastikan tidak ada firewall yang blocking + +## Update Code + +Setiap kali ada perubahan code: + +```bash +git add . +git commit -m "Update: deskripsi perubahan" +git push +``` + +Railway akan otomatis re-deploy setelah push ke GitHub. + +## Free Tier Limits + +Railway free tier: +- $5 credit per bulan +- 500 jam execution time +- 1GB RAM +- 1GB disk + +Cukup untuk development dan testing! diff --git a/backend-Sigasti/README.md b/backend-Sigasti/README.md new file mode 100644 index 0000000..f254870 --- /dev/null +++ b/backend-Sigasti/README.md @@ -0,0 +1,59 @@ +# Backend Autoclave Sterilization System + +Backend API untuk sistem monitoring dan kontrol autoclave sterilization menggunakan MQTT. + +## Tech Stack +- Node.js + Express +- MongoDB (Mongoose) +- MQTT (HiveMQ) +- CORS + +## Environment Variables + +Buat file `.env` dengan variabel berikut: + +```env +PORT=5000 +MONGODB_URI=your_mongodb_connection_string +``` + +## Installation + +```bash +npm install +``` + +## Running Locally + +```bash +npm start +``` + +Server akan berjalan di `http://localhost:5000` + +## API Endpoints + +### Sterilization Set +- `POST /sterilisasi/set` - Kirim perintah start ke alat +- `GET /sterilisasi/set` - Ambil semua data set +- `GET /sterilisasi/set/last` - Ambil data set terakhir + +### Sterilization Running +- `POST /sterilisasi/running` - Kirim perintah stop ke alat +- `GET /sterilisasi/running` - Ambil semua data running +- `GET /sterilisasi/running/last` - Ambil data running terakhir + +### Sterilization Finish +- `GET /sterilisasi/finish` - Ambil semua data finish +- `GET /sterilisasi/finish/last` - Ambil data finish terakhir (consumed) + +### History +- `GET /sterilisasi/history` - Ambil history lengkap (gabungan finish + set) + +## Deployment + +Deploy ke Railway: +1. Push code ke GitHub +2. Connect repository di Railway +3. Set environment variables di Railway dashboard +4. Deploy otomatis akan berjalan diff --git a/backend-Sigasti/config/database.js b/backend-Sigasti/config/database.js new file mode 100644 index 0000000..288c44b --- /dev/null +++ b/backend-Sigasti/config/database.js @@ -0,0 +1,23 @@ +const mongoose = require("mongoose"); + +const connectDB = async () => { + try { + // Support both MONGODB_URI (Railway default) and MONGO_URI (local .env) + const mongoUri = process.env.MONGODB_URI || process.env.MONGO_URI; + + if (!mongoUri) { + console.error("❌ MONGODB_URI atau MONGO_URI tidak ditemukan!"); + console.log("Available env vars:", Object.keys(process.env).filter(k => k.includes('MONGO'))); + process.exit(1); + } + + console.log("🔗 Connecting to MongoDB..."); + await mongoose.connect(mongoUri); + console.log("MongoDB Connected ✅"); + } catch (error) { + console.error("MongoDB Error ❌:", error.message); + process.exit(1); + } +}; + +module.exports = connectDB; \ No newline at end of file diff --git a/backend-Sigasti/models/sterilisasi.js b/backend-Sigasti/models/sterilisasi.js new file mode 100644 index 0000000..1a092b3 --- /dev/null +++ b/backend-Sigasti/models/sterilisasi.js @@ -0,0 +1,108 @@ +const mongoose = require("mongoose"); + +// ── Koleksi: data dari topik sterilisasi/set ────────────── +// Menyimpan perintah yang dikirim dari aplikasi ke alat +const setSchema = new mongoose.Schema({ + action: { type: String }, + suhu: { type: Number }, + tekanan: { type: Number }, + waktu: { type: mongoose.Schema.Types.Mixed }, + device: { type: String }, + namaAlat: { type: String, default: "" }, + status: { type: String, default: "unknown" }, + batch_id: { type: String }, // ID unik untuk membedakan proses + createdAt:{ type: Date, default: Date.now }, +}); + +// ── Koleksi: data dari topik sterilisasi/running ────────── +// Menyimpan status proses yang dikirim alat ke backend +const runningSchema = new mongoose.Schema({ + action: { type: String }, + suhu: { type: Number }, + tekanan: { type: Number }, + waktu: { type: mongoose.Schema.Types.Mixed }, + timer: { type: String }, // Timer dari alat (format: "00:00:00") + device: { type: String }, + sesi: { type: String }, + status: { type: String }, + percobaan: { type: Number }, // Jumlah percobaan ignition (untuk ignition_failed) + batch_id: { type: String }, // ID unik untuk membedakan proses + createdAt: { type: Date, default: Date.now }, +}); + +// ── Koleksi: data dari topik sterilisasi/finish ─────────── +// Menyimpan data saat proses selesai atau dihentikan +const finishSchema = new mongoose.Schema({ + action: { type: String, default: "finish" }, // "finish" atau "stop" + suhu: { type: Number }, + tekanan: { type: Number }, + waktu: { type: mongoose.Schema.Types.Mixed }, + device: { type: String }, + notes: { type: String, default: "" }, // Catatan user untuk riwayat + batch_id: { type: String }, // ID unik untuk membedakan proses + createdAt: { type: Date, default: Date.now }, +}); + +// ── Koleksi: data dari topik sterilisasi/manual ─────────── +// Menyimpan perintah manual control (valve, gas, starter) dan data real-time (suhu, tekanan) +const manualSchema = new mongoose.Schema({ + valve: { type: String }, // "OPEN" atau "CLOSE" + gas: { type: String }, // "TUTUP", "KECIL", "SEDANG", "BESAR" + starter: { type: String }, // "ON" atau "OFF" + suhureal: { type: Number }, // Suhu real-time dari alat + tekananreal: { type: Number }, // Tekanan real-time dari alat + device: { type: String }, + source: { type: String, default: "device" }, // "device" atau "backend" (anti-loop) + createdAt: { type: Date, default: Date.now }, +}); + +// ── Koleksi BARU: histories ──────────────────────────────── +// Menyimpan semua data proses dalam satu document berdasarkan batch_id +const historySchema = new mongoose.Schema({ + batch_id: { type: String, required: true, unique: true, index: true }, // Primary key + device: { type: String, required: true }, + namaAlat: { type: String, default: "" }, + + // Data dari SET (awal proses) + set: { + suhu: { type: Number }, + tekanan: { type: Number }, + waktu: { type: String }, + startedAt: { type: Date }, + }, + + // Data RUNNING (array untuk grafik) + runningData: [{ + suhu: { type: Number }, + tekanan: { type: Number }, + timer: { type: String }, + timestamp: { type: Date, default: Date.now }, + _id: false // Disable _id untuk subdocument + }], + + // Data FINISH + finish: { + action: { type: String }, // "finish" atau "stop" + suhu: { type: Number }, + tekanan: { type: Number }, + finishedAt: { type: Date }, + }, + + status: { + type: String, + enum: ['running', 'completed', 'stopped'], + default: 'running' + }, + notes: { type: String, default: "" }, + + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +const Set = mongoose.model("Set", setSchema); +const Running = mongoose.model("Running", runningSchema); +const Finish = mongoose.model("Finish", finishSchema); +const Manual = mongoose.model("Manual", manualSchema); +const History = mongoose.model("History", historySchema); + +module.exports = { Set, Running, Finish, Manual, History }; diff --git a/backend-Sigasti/mqtt/historyHelper.js b/backend-Sigasti/mqtt/historyHelper.js new file mode 100644 index 0000000..0d4f8b6 --- /dev/null +++ b/backend-Sigasti/mqtt/historyHelper.js @@ -0,0 +1,133 @@ +const { History } = require("../models/sterilisasi"); + +/** + * Helper functions untuk mengelola collection History + */ + +/** + * Buat history baru saat menerima SET (start) + */ +async function createHistory(data) { + const batchId = data.batch_id; + + if (!batchId) { + console.log("[History] Tidak ada batch_id, skip create history"); + return null; + } + + try { + const history = await History.create({ + batch_id: batchId, + device: data.Device ?? data.device, + namaAlat: data.namaAlat ?? "", + set: { + suhu: data.suhu ?? null, + tekanan: data.tekanan ?? null, + waktu: data.waktu ?? null, + startedAt: new Date(), + }, + runningData: [], + status: 'running', + }); + + console.log(`[History] ✅ Created: batch_id=${batchId}`); + return history; + } catch (error) { + if (error.code === 11000) { + console.log(`[History] Sudah ada: batch_id=${batchId}`); + } else { + console.error("[History] Error create:", error.message); + } + return null; + } +} + +/** + * Tambah data running ke history + */ +async function addRunningData(data) { + const batchId = data.batch_id; + + if (!batchId) { + return null; + } + + try { + const history = await History.findOneAndUpdate( + { batch_id: batchId }, + { + $push: { + runningData: { + suhu: data.suhu ?? null, + tekanan: data.tekanan ?? null, + timer: data.timer ?? null, + timestamp: new Date(), + } + }, + $set: { updatedAt: new Date() } + }, + { new: true } + ); + + if (history) { + console.log(`[History] ✅ Running data added: batch_id=${batchId}, total=${history.runningData.length}`); + } else { + console.log(`[History] ⚠️ History not found for batch_id=${batchId}`); + } + + return history; + } catch (error) { + console.error("[History] Error add running:", error.message); + return null; + } +} + +/** + * Update history dengan data finish + */ +async function finishHistory(data) { + const batchId = data.batch_id; + + if (!batchId) { + return null; + } + + const action = data.action ?? "finish"; + const newStatus = action === "stop" ? "stopped" : "completed"; + + try { + const history = await History.findOneAndUpdate( + { batch_id: batchId }, + { + $set: { + finish: { + action: action, + suhu: data.suhu ?? null, + tekanan: data.tekanan ?? null, + finishedAt: new Date(), + }, + status: newStatus, + updatedAt: new Date(), + } + }, + { new: true } + ); + + if (history) { + console.log(`[History] ✅ Finished: batch_id=${batchId}, status=${newStatus}`); + } else { + console.log(`[History] ⚠️ History not found for batch_id=${batchId}`); + } + + return history; + } catch (error) { + console.error("[History] Error finish:", error.message); + return null; + } +} + +module.exports = { + createHistory, + addRunningData, + finishHistory, +}; diff --git a/backend-Sigasti/mqtt/mqttClient.js b/backend-Sigasti/mqtt/mqttClient.js new file mode 100644 index 0000000..6fce5dc --- /dev/null +++ b/backend-Sigasti/mqtt/mqttClient.js @@ -0,0 +1,321 @@ +require("dotenv").config(); +const mqtt = require("mqtt"); +const broker = "mqtt://broker.hivemq.com"; +const { Set, Running, Finish, Manual, History } = require("../models/sterilisasi"); +const { createHistory, addRunningData, finishHistory } = require("./historyHelper"); + +/** + * TOPIK MQTT: + * + * SUBSCRIBE (menerima dari perangkat): + * sterilisasi/running — respon proses (countdown, running, ignition) + * sterilisasi/finish — sinyal selesai dari perangkat + * sterilisasi/set — logging perintah yang dikirim ke perangkat + * sterilisasi/manual — logging perintah manual control + * + * PUBLISH (mengirim ke perangkat): + * sterilisasi/set — perintah start dari aplikasi + * sterilisasi/finish — perintah stop dari aplikasi + * sterilisasi/manual — perintah manual control (valve, gas, starter) + */ + +const SUBSCRIBE_TOPIC = "sterilisasi/running"; +const FINISH_TOPIC = "sterilisasi/finish"; +const SET_TOPIC = "sterilisasi/set"; +const MANUAL_TOPIC = "sterilisasi/manual"; +const PUBLISH_TOPIC = "sterilisasi/set"; + +const client = mqtt.connect(broker); + +let lastData = null; +let lastFinishData = null; +let lastManualData = null; // Data manual control terbaru +let finishConsumed = false; +let finishTimestamp = null; // Timestamp kapan finish diterima +const FINISH_LOCK_DURATION = 5000; // 5 detik setelah finish, abaikan running + +client.on("connect", () => { + console.log("MQTT Terhubung ✅ →", broker); + + client.subscribe(SUBSCRIBE_TOPIC, (err) => { + if (!err) console.log("Subscribe ke topic:", SUBSCRIBE_TOPIC); + else console.error("Gagal subscribe:", err.message); + }); + + client.subscribe(FINISH_TOPIC, (err) => { + if (!err) console.log("Subscribe ke topic:", FINISH_TOPIC); + else console.error("Gagal subscribe:", err.message); + }); + + client.subscribe(SET_TOPIC, (err) => { + if (!err) console.log("Subscribe ke topic:", SET_TOPIC); + else console.error("Gagal subscribe:", err.message); + }); + + client.subscribe(MANUAL_TOPIC, (err) => { + if (!err) console.log("Subscribe ke topic:", MANUAL_TOPIC); + else console.error("Gagal subscribe:", err.message); + }); +}); + +client.on("message", async (receivedTopic, message) => { + const raw = message.toString(); + console.log(`[${receivedTopic}] Data masuk:`, raw); + + let data = {}; + try { + data = JSON.parse(raw); + } catch { + console.warn("Payload bukan JSON, diabaikan"); + return; + } + + // ── Topik sterilisasi/set ───────────────────────────────── + if (receivedTopic === SET_TOPIC) { + const action = data.action ?? null; + + // ANTI-LOOP: Abaikan message yang dikirim oleh backend sendiri + if (data.source === "backend") { + console.log("[sterilisasi/set] Message dari backend sendiri, diabaikan (anti-loop)"); + return; + } + + console.log(`[sterilisasi/set] Menerima perintah: ${action}, batch_id: ${data.batch_id ?? 'tidak ada'}`); + try { + const setData = { + action, + device: data.Device ?? data.device ?? "unknown", + namaAlat: data.namaAlat ?? "", + status: action === "start" ? "running" : (action === "stop" ? "dihentikan" : "unknown"), + batch_id: data.batch_id ?? null, // Simpan batch_id + }; + if (data.suhu != null) setData.suhu = data.suhu; + if (data.tekanan != null) setData.tekanan = data.tekanan; + if (data.waktu != null) setData.waktu = data.waktu; + await new Set(setData).save(); + console.log(`[sterilisasi/set] Disimpan: action=${action}, batch_id=${setData.batch_id}`); + + // ── Simpan ke History (jika action=start dan ada batch_id) ── + if (action === "start" && setData.batch_id) { + await createHistory(data); + } + } catch (error) { + console.error("[sterilisasi/set] Gagal simpan:", error.message); + } + return; + } + + // ── Topik sterilisasi/finish ────────────────────────────── + if (receivedTopic === FINISH_TOPIC) { + const action = data.action ?? "finish"; // Default action adalah "finish" + + // ANTI-LOOP: Abaikan message yang dikirim oleh backend sendiri + if (data.source === "backend") { + console.log("[sterilisasi/finish] Message dari backend sendiri, diabaikan (anti-loop)"); + return; + } + + lastFinishData = { + action: action, // Bisa "finish" atau "stop" + suhu: data.suhu ?? null, + tekanan: data.tekanan ?? null, + waktu: data.waktu ?? null, + device: data.Device ?? data.device ?? null, + batch_id: data.batch_id ?? null, // Simpan batch_id + }; + finishConsumed = false; + finishTimestamp = Date.now(); // Catat waktu finish diterima + console.log(`[sterilisasi/finish] lastFinishData diperbarui (action=${action}, batch_id=${lastFinishData.batch_id}):`, lastFinishData); + + // Update lastData juga agar frontend bisa detect action finish/stop + lastData = { + action: action, + suhu: data.suhu ?? null, + tekanan: data.tekanan ?? null, + waktu: data.waktu ?? null, + timer: null, + device: data.Device ?? data.device ?? null, + sesi: null, + status: null, + percobaan: null, + batch_id: data.batch_id ?? null, // Simpan batch_id + }; + console.log(`[sterilisasi/finish] lastData diperbarui dengan action ${action}, batch_id=${lastData.batch_id}:`, lastData); + + try { + await new Finish(lastFinishData).save(); + console.log(`[sterilisasi/finish] Disimpan ke collection Finish (action=${action}, batch_id=${lastFinishData.batch_id})`); + + // ── Update History dengan data finish (jika ada batch_id) ── + if (lastFinishData.batch_id) { + await finishHistory(lastFinishData); + } + } catch (error) { + console.error("[sterilisasi/finish] Gagal simpan:", error.message); + } + return; + } + + // ── Topik sterilisasi/manual ────────────────────────────── + if (receivedTopic === MANUAL_TOPIC) { + // ANTI-LOOP: Abaikan message yang dikirim oleh backend sendiri + if (data.source === "backend") { + console.log("[sterilisasi/manual] Message dari backend sendiri, diabaikan (anti-loop)"); + return; + } + + lastManualData = { + valve: data.valve ?? null, + gas: data.gas ?? null, + starter: data.starter ?? null, + suhureal: data.suhureal ?? null, + tekananreal: data.tekananreal ?? null, + device: data.device ?? data.Device ?? null, + source: data.source ?? "device", // Track source + }; + console.log("[sterilisasi/manual] lastManualData diperbarui (source: device, TIDAK disimpan ke DB):", lastManualData); + + // TIDAK DISIMPAN KE DATABASE - hanya di memory untuk real-time access + // try { + // await new Manual(lastManualData).save(); + // console.log("[sterilisasi/manual] Disimpan ke collection Manual"); + // } catch (error) { + // console.error("[sterilisasi/manual] Gagal simpan:", error.message); + // } + return; + } + + // ── Topik sterilisasi/running ───────────────────────────── + const action = data.action ?? null; + if (!action) { + console.warn("Tidak ada field action, diabaikan"); + return; + } + + // ANTI-LOOP: Abaikan message yang dikirim oleh backend sendiri + if (data.source === "backend") { + console.log("[sterilisasi/running] Message dari backend sendiri, diabaikan (anti-loop)"); + return; + } + + const validActions = ["countdown", "running", "ignition", "ignition_failed", "stop"]; + if (!validActions.includes(action)) { + console.warn("Action tidak dikenali:", action); + return; + } + + // WORKAROUND: Abaikan data running jika baru saja menerima finish (dalam 5 detik) + if (action === "running" && finishTimestamp && (Date.now() - finishTimestamp < FINISH_LOCK_DURATION)) { + console.warn(`[WORKAROUND] Data running diabaikan karena finish baru diterima ${Date.now() - finishTimestamp}ms yang lalu`); + return; + } + + lastData = { + action, + suhu: data.suhu ?? null, + tekanan: data.tekanan ?? null, + waktu: data.waktu ?? null, // Tidak menggunakan fallback, biarkan null jika tidak ada + timer: data.timer ?? null, // Timer dari alat (format: "00:00:00") + device: data.Device ?? data.device ?? null, + sesi: data.sesi ?? null, + status: data.status ?? null, + percobaan: data.percobaan ?? null, // Jumlah percobaan ignition (untuk ignition_failed) + batch_id: data.batch_id ?? null, // Simpan batch_id + }; + console.log("lastData diperbarui:", lastData); + + try { + await new Running(lastData).save(); + console.log(`[sterilisasi/running] Disimpan: action=${action}, batch_id=${lastData.batch_id}`); + + // ── Tambah ke History.runningData (jika ada batch_id) ── + if (lastData.batch_id && action === "running") { + await addRunningData(lastData); + } + } catch (error) { + console.error("[sterilisasi/running] Gagal simpan:", error.message); + } +}); + +module.exports = { + client, + getLastData: () => lastData, + consumeAction: () => { if (lastData) lastData.action = null; }, + getLastFinishData: () => lastFinishData, + consumeFinish: () => { lastFinishData = null; finishConsumed = true; }, + isFinishConsumed: () => finishConsumed, + getLastManualData: () => lastManualData, + PUBLISH_TOPIC, + MANUAL_TOPIC, + updateLastDataWithStop: (device, batch_id = null) => { + lastData = { + action: "stop", + suhu: null, + tekanan: null, + waktu: null, + timer: null, + device: device ?? null, + sesi: null, + status: null, + percobaan: null, + batch_id: batch_id, + }; + console.log(`[MQTT] lastData diperbarui dengan action stop (dari frontend), batch_id=${batch_id}:`, lastData); + }, + publishSet: (payload) => { + return new Promise((resolve, reject) => { + // Tambahkan field source: "backend" untuk anti-loop + const payloadWithSource = { + ...payload, + source: "backend" + }; + + client.publish(PUBLISH_TOPIC, JSON.stringify(payloadWithSource), (err) => { + if (err) reject(err); + else { console.log(`[PUBLISH] ${PUBLISH_TOPIC}:`, JSON.stringify(payloadWithSource)); resolve(); } + }); + }); + }, + publishStop: (payload) => { + return new Promise((resolve, reject) => { + // Tambahkan field source: "backend" untuk anti-loop + const payloadWithSource = { + ...payload, + source: "backend" + }; + + client.publish(FINISH_TOPIC, JSON.stringify(payloadWithSource), (err) => { + if (err) reject(err); + else { console.log(`[PUBLISH] ${FINISH_TOPIC}:`, JSON.stringify(payloadWithSource)); resolve(); } + }); + }); + }, + publishManual: (payload) => { + return new Promise((resolve, reject) => { + // Tambahkan field source: "backend" untuk anti-loop + const payloadWithSource = { + ...payload, + source: "backend" + }; + + client.publish(MANUAL_TOPIC, JSON.stringify(payloadWithSource), (err) => { + if (err) reject(err); + else { console.log(`[PUBLISH] ${MANUAL_TOPIC}:`, JSON.stringify(payloadWithSource)); resolve(); } + }); + }); + }, + publishRunning: (payload) => { + return new Promise((resolve, reject) => { + // Tambahkan field source: "backend" untuk anti-loop + const payloadWithSource = { + ...payload, + source: "backend" + }; + + client.publish(SUBSCRIBE_TOPIC, JSON.stringify(payloadWithSource), (err) => { + if (err) reject(err); + else { console.log(`[PUBLISH] ${SUBSCRIBE_TOPIC}:`, JSON.stringify(payloadWithSource)); resolve(); } + }); + }); + }, +}; diff --git a/backend-Sigasti/package-lock.json b/backend-Sigasti/package-lock.json new file mode 100644 index 0000000..5dd36bc --- /dev/null +++ b/backend-Sigasti/package-lock.json @@ -0,0 +1,1437 @@ +{ + "name": "back-end", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "back-end", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.4.1", + "express": "^5.2.1", + "mongoose": "^9.4.1", + "mqtt": "^5.15.1" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/broker-factory": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.14.tgz", + "integrity": "sha512-L45k5HMbPIrMid0nTOZ/UPXG/c0aRuQKVrSDFIb1zOkvfiyHgYmIjc3cSiN1KwQIvRDOtKE0tfb3I9EZ3CmpQQ==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.49" + } + }, + "node_modules/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/kareem": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz", + "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mongodb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.1.tgz", + "integrity": "sha512-067DXiMjcpYQl6bGjWQoTUEE9UoRViTtKFcoqX7z08I+iDZv/emH1g8XEFiO3qiDfXAheT5ozl1VffDTKhIW/w==", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.1.1", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.4.1.tgz", + "integrity": "sha512-4rFBWa+/wdBQSfvnOPJBpiSG6UCEbhSQh865dEdaH9Y8WfHBUC+I2XT28dp0IBIGrEwmh+gzrgZgea5PbmrHWA==", + "dependencies": { + "kareem": "3.2.0", + "mongodb": "~7.1", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mqtt": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz", + "integrity": "sha512-V1WnkGuJh3ec9QXzy5Iylw8OOBK+Xu1WhxcQ9mMpLThG+/JZIMV1PgLNRgIiqXhZnvnVLsuyxHl5A/3bHHbcAA==", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", + "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/worker-factory": { + "version": "7.0.49", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.49.tgz", + "integrity": "sha512-lW7tpgy6aUv2dFsQhv1yv+XFzdkCf/leoKRTGMPVK5/die6RrUjqgJHJf556qO+ZfytNG6wPXc17E8zzsOLUDw==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.31", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.31.tgz", + "integrity": "sha512-ngkq5S6JuZyztom8tDgBzorLo9byhBMko/sXfgiUD945AuzKGg1GCgDMCC3NaYkicLpGKXutONM36wEX8UbBCA==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.16", + "worker-timers-worker": "^9.0.14" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.16.tgz", + "integrity": "sha512-JyP3AvUGyPGbBGW7XiUewm2+0pN/aYo1QpVf5kdXAfkDZcN3p7NbWrG6XnyDEpDIvfHk/+LCnOW/NsuiU9riYA==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "broker-factory": "^3.1.14", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.14" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.14.tgz", + "integrity": "sha512-/qF06C60sXmSLfUl7WglvrDIbspmPOM8UrG63Dnn4bi2x4/DfqHS/+dxF5B+MdHnYO5tVuZYLHdAodrKdabTIg==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1", + "worker-factory": "^7.0.49" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/backend-Sigasti/package.json b/backend-Sigasti/package.json new file mode 100644 index 0000000..b8e07fc --- /dev/null +++ b/backend-Sigasti/package.json @@ -0,0 +1,20 @@ +{ + "name": "back-end", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.4.1", + "express": "^5.2.1", + "mongoose": "^9.4.1", + "mqtt": "^5.15.1" + } +} diff --git a/backend-Sigasti/railway.toml b/backend-Sigasti/railway.toml new file mode 100644 index 0000000..17f1d05 --- /dev/null +++ b/backend-Sigasti/railway.toml @@ -0,0 +1,7 @@ +[build] +builder = "NIXPACKS" + +[deploy] +startCommand = "node server.js" +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 10 diff --git a/backend-Sigasti/routes/sterilisasi.js b/backend-Sigasti/routes/sterilisasi.js new file mode 100644 index 0000000..e96a5e4 --- /dev/null +++ b/backend-Sigasti/routes/sterilisasi.js @@ -0,0 +1,433 @@ +const express = require("express"); +const router = express.Router(); +const { Set, Running, Finish, Manual, History } = require("../models/sterilisasi"); +const mqttClient = require("../mqtt/mqttClient"); + +// ── POST /sterilisasi/set ───────────────────────────────────── +// Frontend kirim parameter start → publish ke MQTT sterilisasi/set +router.post("/set", async (req, res) => { + try { + const { action, suhu, tekanan, waktu, device, batch_id } = req.body; + + if (!device) { + return res.status(400).json({ status: "error", message: "Field device wajib diisi" }); + } + + const mqttPayload = { + action: action || "start", + suhu: suhu != null ? Number(suhu) : null, + tekanan: tekanan != null ? Number(tekanan) : null, + waktu, + Device: device, + batch_id: batch_id ?? null, // Include batch_id jika ada + }; + + await mqttClient.publishSet(mqttPayload); + + res.json({ status: "success", message: `Perintah ${action || "start"} berhasil dikirim`, batch_id: batch_id }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/set ────────────────────────────────────── +router.get("/set", async (_req, res) => { + try { + const data = await Set.find().sort({ createdAt: -1 }).limit(50); + res.json({ status: "success", data }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/set/last ───────────────────────────────── +router.get("/set/last", async (_req, res) => { + try { + const last = await Set.findOne().sort({ createdAt: -1 }); + res.json({ status: "success", data: last }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/running ────────────────────────────────── +router.get("/running", async (_req, res) => { + try { + const data = await Running.find().sort({ createdAt: -1 }).limit(50); + res.json({ status: "success", data }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── POST /sterilisasi/running ───────────────────────────────── +// Frontend kirim perintah stop → publish ke topik sterilisasi/running +router.post("/running", async (req, res) => { + try { + const { action, device, suhu, tekanan, batch_id } = req.body; + + if (!device) { + return res.status(400).json({ status: "error", message: "Field device wajib diisi" }); + } + + // Jika action adalah stop, kirim ke topic sterilisasi/finish + if (action === "stop" || !action) { + const mqttPayload = { + action: "stop", + suhu: suhu != null ? Number(suhu) : 0, + tekanan: tekanan != null ? Number(tekanan) : 0, + device: device, + batch_id: batch_id ?? null, // Include batch_id jika ada + }; + + await mqttClient.publishStop(mqttPayload); + + // Update lastData langsung agar frontend bisa detect stop + mqttClient.updateLastDataWithStop(device, batch_id); + + return res.json({ + status: "success", + message: `Perintah stop berhasil dikirim ke topik sterilisasi/finish`, + batch_id: batch_id + }); + } + + // Untuk action lain, kirim ke topic sterilisasi/running + const mqttPayload = { + action: action, + Device: device, + batch_id: batch_id ?? null, // Include batch_id jika ada + }; + + await mqttClient.publishRunning(mqttPayload); + + res.json({ status: "success", message: `Perintah ${action} berhasil dikirim ke topik running`, batch_id: batch_id }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/running/last ───────────────────────────── +router.get("/running/last", async (_req, res) => { + try { + const last = await Running.findOne().sort({ createdAt: -1 }); + res.json({ status: "success", data: last }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/running/batch/:batch_id ────────────────── +// Ambil semua data running berdasarkan batch_id untuk grafik riwayat +router.get("/running/batch/:batch_id", async (req, res) => { + try { + const { batch_id } = req.params; + + if (!batch_id) { + return res.status(400).json({ status: "error", message: "batch_id tidak valid" }); + } + + // Ambil semua data running dengan batch_id yang sama, diurutkan berdasarkan waktu + const runningData = await Running.find({ batch_id }).sort({ createdAt: 1 }); + + res.json({ + status: "success", + data: runningData, + count: runningData.length + }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/finish ─────────────────────────────────── +router.get("/finish", async (_req, res) => { + try { + const data = await Finish.find().sort({ createdAt: -1 }).limit(100); + res.json({ status: "success", data }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/histories ──────────────────────────────── +// Ambil semua data dari collection histories (yang punya batch_id) +router.get("/histories", async (_req, res) => { + try { + const histories = await History.find() + .sort({ createdAt: -1 }) + .limit(100); + + // Transform data untuk frontend + const data = histories.map(h => ({ + _id: h._id, + batch_id: h.batch_id, + device: h.device, + namaAlat: h.namaAlat, + suhu: h.set?.suhu ?? 0, + tekanan: h.set?.tekanan ?? 0, + waktu: h.set?.waktu ?? "00:00", + status: h.status, + action: h.finish?.action ?? null, + createdAt: h.createdAt, + notes: h.notes ?? "", + runningDataCount: h.runningData?.length ?? 0, + })); + + res.json({ status: "success", data }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/histories/:batch_id ────────────────────── +// Ambil satu history lengkap dengan runningData untuk grafik +router.get("/histories/:batch_id", async (req, res) => { + try { + const { batch_id } = req.params; + + const history = await History.findOne({ batch_id }); + + if (!history) { + return res.status(404).json({ + status: "error", + message: "History not found" + }); + } + + res.json({ status: "success", data: history }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/history ────────────────────────────────── +// DEPRECATED: Masih ada untuk backward compatibility +// Menggabungkan data finish dengan set untuk history lengkap +router.get("/history", async (_req, res) => { + try { + const finishData = await Finish.find().sort({ createdAt: -1 }).limit(100); + + // Untuk setiap finish, cari set yang sesuai berdasarkan batch_id atau device+waktu + const history = await Promise.all( + finishData.map(async (finish) => { + let matchingSet = null; + let batchIdToUse = finish.batch_id; // Default: gunakan batch_id dari finish + + // Prioritas 1: Cari berdasarkan batch_id jika ada + if (finish.batch_id) { + matchingSet = await Set.findOne({ + batch_id: finish.batch_id, + action: "start" + }).sort({ createdAt: -1 }); + + if (matchingSet) { + console.log(`[History] Match berdasarkan batch_id: ${finish.batch_id}`); + } + } + + // Prioritas 2: Fallback ke metode lama (device + waktu) jika tidak ada batch_id atau tidak ketemu + if (!matchingSet) { + const timeWindow = new Date(finish.createdAt.getTime() - 2 * 60 * 60 * 1000); + matchingSet = await Set.findOne({ + device: finish.device, + action: "start", + createdAt: { $gte: timeWindow, $lte: finish.createdAt } + }).sort({ createdAt: -1 }); + + if (matchingSet) { + console.log(`[History] Match berdasarkan device+waktu untuk device: ${finish.device}`); + + // FALLBACK: Jika finish tidak punya batch_id tapi set punya, gunakan dari set + if (!batchIdToUse && matchingSet.batch_id) { + batchIdToUse = matchingSet.batch_id; + console.log(`[History] Menggunakan batch_id dari Set: ${batchIdToUse}`); + } + } + } + + // FALLBACK FINAL: Jika masih tidak ada batch_id, coba cari dari running terakhir + if (!batchIdToUse) { + const timeWindow = new Date(finish.createdAt.getTime() - 2 * 60 * 60 * 1000); + const lastRunning = await Running.findOne({ + device: finish.device, + batch_id: { $ne: null }, // Hanya yang ada batch_id + createdAt: { $gte: timeWindow, $lte: finish.createdAt } + }).sort({ createdAt: -1 }); + + if (lastRunning && lastRunning.batch_id) { + batchIdToUse = lastRunning.batch_id; + console.log(`[History] Menggunakan batch_id dari Running: ${batchIdToUse}`); + } + } + + return { + _id: finish._id, + action: finish.action || "finish", // Include action dari finish ("finish" atau "stop") + device: finish.device, + suhu: matchingSet?.suhu ?? finish.suhu ?? 0, + tekanan: matchingSet?.tekanan ?? finish.tekanan ?? 0, + waktu: matchingSet?.waktu ?? finish.waktu ?? "00:00", + finishSuhu: finish.suhu, + finishTekanan: finish.tekanan, + createdAt: finish.createdAt, + notes: finish.notes || "", + batch_id: batchIdToUse, // Include batch_id (dari finish, set, atau running) + }; + }) + ); + + res.json({ status: "success", data: history }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/finish/last ────────────────────────────── +// Ambil data finish dari memory dan CONSUME setelah dibaca +router.get("/finish/last", async (_req, res) => { + try { + const lastFinish = mqttClient.getLastFinishData(); + + if (!lastFinish) { + return res.json({ status: "success", data: null }); + } + + // Copy dulu sebelum di-consume agar tidak hilang karena race condition + const dataToSend = { ...lastFinish }; + mqttClient.consumeFinish(); + + console.log("[GET /sterilisasi/finish/last] Data finish di-consume:", JSON.stringify(dataToSend)); + + res.json({ status: "success", data: dataToSend }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── DELETE /sterilisasi/history/:id ─────────────────────────── +// Hapus riwayat berdasarkan ID +router.delete("/history/:id", async (req, res) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ status: "error", message: "ID tidak valid" }); + } + + const deleted = await Finish.findByIdAndDelete(id); + + if (!deleted) { + return res.status(404).json({ status: "error", message: "Data tidak ditemukan" }); + } + + res.json({ status: "success", message: "Riwayat berhasil dihapus" }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── PATCH /sterilisasi/history/:id ──────────────────────────── +// Update catatan riwayat berdasarkan ID +router.patch("/history/:id", async (req, res) => { + try { + const { id } = req.params; + const { notes } = req.body; + + if (!id) { + return res.status(400).json({ status: "error", message: "ID tidak valid" }); + } + + const updated = await Finish.findByIdAndUpdate( + id, + { notes: notes || "" }, + { returnDocument: 'after' } // Updated: use returnDocument instead of new + ); + + if (!updated) { + return res.status(404).json({ status: "error", message: "Data tidak ditemukan" }); + } + + res.json({ status: "success", message: "Catatan berhasil diupdate", data: updated }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── POST /sterilisasi/manual ────────────────────────────────── +// Frontend kirim perintah manual control → publish ke MQTT sterilisasi/manual +router.post("/manual", async (req, res) => { + try { + const { valve, gas, starter, suhureal, tekananreal, device } = req.body; + + if (!device) { + return res.status(400).json({ status: "error", message: "Field device wajib diisi" }); + } + + // Validasi nilai valve + if (valve && !["OPEN", "CLOSE"].includes(valve)) { + return res.status(400).json({ + status: "error", + message: "Nilai valve harus OPEN atau CLOSE" + }); + } + + // Validasi nilai gas + if (gas && !["TUTUP", "KECIL", "SEDANG", "BESAR"].includes(gas)) { + return res.status(400).json({ + status: "error", + message: "Nilai gas harus TUTUP, KECIL, SEDANG, atau BESAR" + }); + } + + // Validasi nilai starter + if (starter && !["ON", "OFF"].includes(starter)) { + return res.status(400).json({ + status: "error", + message: "Nilai starter harus ON atau OFF" + }); + } + + const mqttPayload = { + valve: valve ?? null, + gas: gas ?? null, + starter: starter ?? null, + suhureal: suhureal != null ? Number(suhureal) : null, + tekananreal: tekananreal != null ? Number(tekananreal) : null, + device: device, + }; + + await mqttClient.publishManual(mqttPayload); + + res.json({ + status: "success", + message: "Perintah manual control berhasil dikirim", + data: mqttPayload + }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/manual/last ────────────────────────────── +// Ambil data manual control terbaru dari memory +router.get("/manual/last", async (_req, res) => { + try { + const lastManual = mqttClient.getLastManualData(); + res.json({ status: "success", data: lastManual }); + } catch (error) { + res.status(500).json({ status: "error", message: error.message }); + } +}); + +// ── GET /sterilisasi/manual ─────────────────────────────────── +// History manual control - DISABLED (data tidak disimpan ke database) +router.get("/manual", async (_req, res) => { + res.json({ + status: "info", + message: "History manual control tidak tersedia. Data hanya disimpan di memory untuk real-time access.", + data: [] + }); +}); + +module.exports = router; diff --git a/backend-Sigasti/server.js b/backend-Sigasti/server.js new file mode 100644 index 0000000..b8e3e66 --- /dev/null +++ b/backend-Sigasti/server.js @@ -0,0 +1,25 @@ +require("dotenv").config(); // Load env vars PERTAMA KALI + +const express = require("express"); +const cors = require("cors"); +const connectDB = require("./config/database"); + +// Inisialisasi MQTT client (subscribe otomatis saat require) +require("./mqtt/mqttClient"); + +const sterilisasiRouter = require("./routes/sterilisasi"); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +connectDB(); + +// ── Routes ──────────────────────────────────────────────── +app.use("/sterilisasi", sterilisasiRouter); + +app.get("/", (_req, res) => res.send("Backend MQTT Aktif 🚀")); + +// ───────────────────────────────────────────────────────── +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => console.log(`Server jalan di http://localhost:${PORT}`)); diff --git a/backend-Sigasti/test-batch-id.js b/backend-Sigasti/test-batch-id.js new file mode 100644 index 0000000..bddbaf3 --- /dev/null +++ b/backend-Sigasti/test-batch-id.js @@ -0,0 +1,105 @@ +/** + * Script untuk test endpoint batch_id + * + * Usage: node test-batch-id.js + */ + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:5000'; + +async function testHistoryEndpoint() { + console.log('\n=== Test GET /sterilisasi/history ===\n'); + + try { + const response = await fetch(`${BACKEND_URL}/sterilisasi/history`); + const data = await response.json(); + + if (data.status === 'success') { + console.log(`✅ Success! Found ${data.data.length} history entries\n`); + + // Tampilkan 3 entry pertama dengan batch_id + console.log('Sample data (first 3):'); + data.data.slice(0, 3).forEach((item, index) => { + console.log(`\n${index + 1}. ID: ${item._id}`); + console.log(` Device: ${item.device}`); + console.log(` Batch ID: ${item.batch_id || '❌ NULL'}`); + console.log(` Suhu: ${item.suhu}°C`); + console.log(` Tekanan: ${item.tekanan} bar`); + console.log(` Created: ${item.createdAt}`); + }); + + // Cek berapa yang punya batch_id + const withBatchId = data.data.filter(item => item.batch_id); + const withoutBatchId = data.data.filter(item => !item.batch_id); + + console.log(`\n📊 Statistics:`); + console.log(` Total entries: ${data.data.length}`); + console.log(` With batch_id: ${withBatchId.length} ✅`); + console.log(` Without batch_id: ${withoutBatchId.length} ⚠️`); + + return withBatchId.length > 0 ? withBatchId[0].batch_id : null; + } else { + console.log('❌ Failed:', data); + return null; + } + } catch (error) { + console.error('❌ Error:', error.message); + return null; + } +} + +async function testRunningBatchEndpoint(batchId) { + if (!batchId) { + console.log('\n⚠️ Skipping running/batch test - no batch_id available\n'); + return; + } + + console.log(`\n=== Test GET /sterilisasi/running/batch/${batchId} ===\n`); + + try { + const response = await fetch(`${BACKEND_URL}/sterilisasi/running/batch/${batchId}`); + const data = await response.json(); + + if (data.status === 'success') { + console.log(`✅ Success! Found ${data.count} running data points\n`); + + if (data.data && data.data.length > 0) { + console.log('Sample data (first 5):'); + data.data.slice(0, 5).forEach((item, index) => { + console.log(`\n${index + 1}. Action: ${item.action}`); + console.log(` Suhu: ${item.suhu ?? 'null'}°C`); + console.log(` Tekanan: ${item.tekanan ?? 'null'} bar`); + console.log(` Timer: ${item.timer ?? 'null'}`); + console.log(` Created: ${item.createdAt}`); + }); + + // Statistik data + const suhuData = data.data.map(item => item.suhu ?? 0); + const tekananData = data.data.map(item => item.tekanan ?? 0); + + console.log(`\n📊 Data Statistics:`); + console.log(` Total points: ${data.data.length}`); + console.log(` Suhu range: ${Math.min(...suhuData)} - ${Math.max(...suhuData)}°C`); + console.log(` Tekanan range: ${Math.min(...tekananData)} - ${Math.max(...tekananData)} bar`); + console.log(` First timer: ${data.data[0].timer}`); + console.log(` Last timer: ${data.data[data.data.length - 1].timer}`); + } else { + console.log('⚠️ No data found for this batch_id'); + } + } else { + console.log('❌ Failed:', data); + } + } catch (error) { + console.error('❌ Error:', error.message); + } +} + +// Main execution +(async () => { + console.log('🧪 Testing batch_id endpoints...\n'); + console.log(`Backend URL: ${BACKEND_URL}\n`); + + const batchId = await testHistoryEndpoint(); + await testRunningBatchEndpoint(batchId); + + console.log('\n✅ Test complete!\n'); +})(); diff --git a/backend-Sigasti/test-history-api.js b/backend-Sigasti/test-history-api.js new file mode 100644 index 0000000..5f608ff --- /dev/null +++ b/backend-Sigasti/test-history-api.js @@ -0,0 +1,88 @@ +/** + * Quick test untuk endpoint history + * Usage: node test-history-api.js + */ + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:5000'; + +async function testHistoryAPI() { + console.log('🧪 Testing GET /sterilisasi/history\n'); + console.log(`Backend URL: ${BACKEND_URL}\n`); + + try { + const response = await fetch(`${BACKEND_URL}/sterilisasi/history`); + const result = await response.json(); + + if (result.status === 'success' && result.data) { + console.log(`✅ Success! Found ${result.data.length} entries\n`); + + // Tampilkan 5 entry pertama + console.log('═══════════════════════════════════════════════════════\n'); + result.data.slice(0, 5).forEach((item, index) => { + console.log(`${index + 1}. Entry ID: ${item._id}`); + console.log(` Device: ${item.device}`); + console.log(` Batch ID: ${item.batch_id ? '✅ ' + item.batch_id : '❌ NULL'}`); + console.log(` Suhu: ${item.suhu}°C`); + console.log(` Tekanan: ${item.tekanan} bar`); + console.log(` Waktu: ${item.waktu}`); + console.log(` Created: ${new Date(item.createdAt).toLocaleString()}`); + console.log(''); + }); + console.log('═══════════════════════════════════════════════════════\n'); + + // Statistik + const withBatchId = result.data.filter(item => item.batch_id); + const withoutBatchId = result.data.filter(item => !item.batch_id); + + console.log('📊 Statistics:'); + console.log(` Total entries: ${result.data.length}`); + console.log(` With batch_id: ${withBatchId.length} ✅`); + console.log(` Without batch_id: ${withoutBatchId.length} ⚠️`); + console.log(''); + + // Test endpoint batch untuk yang ada batch_id + if (withBatchId.length > 0) { + const testBatchId = withBatchId[0].batch_id; + console.log(`\n🔍 Testing /sterilisasi/running/batch/${testBatchId}\n`); + + const batchResponse = await fetch(`${BACKEND_URL}/sterilisasi/running/batch/${testBatchId}`); + const batchResult = await batchResponse.json(); + + if (batchResult.status === 'success') { + console.log(`✅ Found ${batchResult.count} running data points`); + + if (batchResult.data && batchResult.data.length > 0) { + console.log('\nSample data (first 3):'); + batchResult.data.slice(0, 3).forEach((item, index) => { + console.log(`\n${index + 1}. Timer: ${item.timer}`); + console.log(` Suhu: ${item.suhu ?? 'null'}°C`); + console.log(` Tekanan: ${item.tekanan ?? 'null'} bar`); + }); + + // Data untuk grafik + const suhuData = batchResult.data.map(d => d.suhu ?? 0); + const tekananData = batchResult.data.map(d => d.tekanan ?? 0); + + console.log(`\n📊 Grafik Data:`); + console.log(` Suhu points: ${suhuData.length}`); + console.log(` Suhu range: ${Math.min(...suhuData)} - ${Math.max(...suhuData)}°C`); + console.log(` Tekanan points: ${tekananData.length}`); + console.log(` Tekanan range: ${Math.min(...tekananData)} - ${Math.max(...tekananData)} bar`); + } + } else { + console.log('❌ Failed to get running data:', batchResult); + } + } else { + console.log('⚠️ No entries with batch_id found for testing'); + } + + } else { + console.log('❌ API returned error:', result); + } + } catch (error) { + console.error('❌ Error:', error.message); + } +} + +// Run +testHistoryAPI();