backend Aplikasi
This commit is contained in:
parent
520ad74e8c
commit
0efe07bb45
|
|
@ -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/
|
||||
|
|
@ -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 `<password>` 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!
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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(); }
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
[build]
|
||||
builder = "NIXPACKS"
|
||||
|
||||
[deploy]
|
||||
startCommand = "node server.js"
|
||||
restartPolicyType = "ON_FAILURE"
|
||||
restartPolicyMaxRetries = 10
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}`));
|
||||
|
|
@ -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');
|
||||
})();
|
||||
|
|
@ -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();
|
||||
Loading…
Reference in New Issue