Front-end aplikasi
|
|
@ -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(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
BUNDLE_PATH: "vendor/bundle"
|
||||||
|
BUNDLE_FORCE_RUBY_PLATFORM: 1
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
BACKEND_URL=https://backend-sigasti-production-6cf6.up.railway.app
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: '@react-native',
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
# OSX
|
||||||
|
#
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Xcode
|
||||||
|
#
|
||||||
|
build/
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
xcuserdata
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
DerivedData
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.xcuserstate
|
||||||
|
**/.xcode.env.local
|
||||||
|
|
||||||
|
# Android/IntelliJ
|
||||||
|
#
|
||||||
|
build/
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
local.properties
|
||||||
|
*.iml
|
||||||
|
*.hprof
|
||||||
|
.cxx/
|
||||||
|
*.keystore
|
||||||
|
!debug.keystore
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
|
# node.js
|
||||||
|
#
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
#
|
||||||
|
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||||
|
# screenshots whenever they are needed.
|
||||||
|
# For more information about the recommended setup visit:
|
||||||
|
# https://docs.fastlane.tools/best-practices/source-control/
|
||||||
|
|
||||||
|
**/fastlane/report.xml
|
||||||
|
**/fastlane/Preview.html
|
||||||
|
**/fastlane/screenshots
|
||||||
|
**/fastlane/test_output
|
||||||
|
|
||||||
|
# Bundle artifact
|
||||||
|
*.jsbundle
|
||||||
|
|
||||||
|
# Ruby / CocoaPods
|
||||||
|
**/Pods/
|
||||||
|
/vendor/bundle/
|
||||||
|
|
||||||
|
# Temporary files created by Metro to check the health of the file watcher
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# Yarn
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
arrowParens: 'avoid',
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Koneksi Frontend ke Backend
|
||||||
|
|
||||||
|
## Backend Production URL
|
||||||
|
|
||||||
|
Backend sudah di-deploy di Railway:
|
||||||
|
```
|
||||||
|
https://backend-sterilisasi.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfigurasi
|
||||||
|
|
||||||
|
File yang sudah diupdate:
|
||||||
|
- ✅ `src/config.ts` - BACKEND_URL diset ke production
|
||||||
|
- ✅ `.env` - BACKEND_URL diset ke production
|
||||||
|
|
||||||
|
## Testing Koneksi
|
||||||
|
|
||||||
|
### 1. Test dari Browser/Postman
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test root endpoint
|
||||||
|
curl https://backend-sterilisasi.com/
|
||||||
|
|
||||||
|
# Test API endpoint
|
||||||
|
curl https://backend-sterilisasi.com/sterilisasi/running/last
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test dari Aplikasi
|
||||||
|
|
||||||
|
1. Rebuild aplikasi React Native:
|
||||||
|
```bash
|
||||||
|
# Stop Metro bundler (Ctrl+C)
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
npx react-native start --reset-cache
|
||||||
|
|
||||||
|
# Di terminal lain, run aplikasi
|
||||||
|
npx react-native run-android
|
||||||
|
# atau
|
||||||
|
npx react-native run-ios
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Buka aplikasi dan coba fitur yang menggunakan backend:
|
||||||
|
- Dashboard → pilih alat → Set parameters → Mulai Proses
|
||||||
|
- History → lihat riwayat dari database
|
||||||
|
|
||||||
|
## Switching Environment
|
||||||
|
|
||||||
|
### Development (Local Backend)
|
||||||
|
|
||||||
|
Edit `src/config.ts`:
|
||||||
|
```typescript
|
||||||
|
export const BACKEND_URL = 'http://10.0.2.2:5000'; // Android Emulator
|
||||||
|
// atau
|
||||||
|
export const BACKEND_URL = 'http://192.168.1.155:5000'; // Device fisik
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production (Railway)
|
||||||
|
|
||||||
|
Edit `src/config.ts`:
|
||||||
|
```typescript
|
||||||
|
export const BACKEND_URL = 'https://backend-sterilisasi.com';
|
||||||
|
```
|
||||||
|
|
||||||
|
**PENTING:** Setelah mengubah config, restart Metro bundler dengan `--reset-cache`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error: Network request failed
|
||||||
|
|
||||||
|
1. **Cek koneksi internet** - Pastikan device/emulator terkoneksi internet
|
||||||
|
2. **Cek URL backend** - Pastikan `https://backend-sterilisasi.com` bisa diakses dari browser
|
||||||
|
3. **Cek CORS** - Backend sudah menggunakan CORS, seharusnya tidak ada masalah
|
||||||
|
4. **Clear cache** - Restart Metro bundler dengan `--reset-cache`
|
||||||
|
|
||||||
|
### Error: 404 Not Found
|
||||||
|
|
||||||
|
1. **Cek endpoint** - Pastikan endpoint yang dipanggil benar
|
||||||
|
2. **Cek backend logs** - Lihat logs di Railway dashboard
|
||||||
|
3. **Test dengan curl** - Test endpoint dari terminal/Postman
|
||||||
|
|
||||||
|
### Data tidak muncul
|
||||||
|
|
||||||
|
1. **Cek database** - Pastikan MongoDB Atlas terkoneksi
|
||||||
|
2. **Cek MQTT** - Pastikan alat bisa kirim data ke MQTT broker
|
||||||
|
3. **Cek logs** - Lihat logs di Railway untuk error
|
||||||
|
|
||||||
|
## Monitoring Backend
|
||||||
|
|
||||||
|
1. Buka Railway dashboard: https://railway.app
|
||||||
|
2. Pilih project `backend-sterilisasi`
|
||||||
|
3. Tab "Logs" - Lihat real-time logs
|
||||||
|
4. Tab "Metrics" - Lihat usage (CPU, RAM, Network)
|
||||||
|
5. Tab "Deployments" - Lihat history deployment
|
||||||
|
|
||||||
|
## Update Backend
|
||||||
|
|
||||||
|
Jika ada perubahan di backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Back-end
|
||||||
|
git add .
|
||||||
|
git commit -m "Update: deskripsi perubahan"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
Railway akan otomatis re-deploy dalam beberapa menit.
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Fitur Grafik Perubahan Suhu dan Tekanan
|
||||||
|
|
||||||
|
## Deskripsi
|
||||||
|
Fitur baru yang menampilkan grafik real-time perubahan suhu dan tekanan di layar Kontrol Manual.
|
||||||
|
|
||||||
|
## Lokasi Fitur
|
||||||
|
- **Screen**: `ManualControlScreen.tsx`
|
||||||
|
- **Posisi**: Di bawah card "Monitor Real-Time"
|
||||||
|
|
||||||
|
## Komponen Grafik
|
||||||
|
|
||||||
|
### 1. **Grafik Suhu**
|
||||||
|
- Menampilkan perubahan suhu dalam satuan °C
|
||||||
|
- Warna: Oranye (fire color) - `#FF6B35`
|
||||||
|
- Update otomatis setiap polling interval
|
||||||
|
|
||||||
|
### 2. **Grafik Tekanan**
|
||||||
|
- Menampilkan perubahan tekanan dalam satuan bar
|
||||||
|
- Warna: Biru (accent color) - `#1BD5FF`
|
||||||
|
- Update otomatis setiap polling interval
|
||||||
|
|
||||||
|
## Teknologi
|
||||||
|
- **Library**: `react-native-chart-kit` v6.12.3
|
||||||
|
- **Dependency**: `react-native-svg` v15.15.5
|
||||||
|
- **Chart Type**: Line Chart dengan Bezier curves
|
||||||
|
|
||||||
|
## Fitur Grafik
|
||||||
|
- ✅ Real-time data visualization
|
||||||
|
- ✅ Maximum 20 data points (sliding window)
|
||||||
|
- ✅ Responsive chart width
|
||||||
|
- ✅ Time-based labels (dalam detik)
|
||||||
|
- ✅ Smooth line dengan bezier curves
|
||||||
|
- ✅ Grid lines untuk kemudahan pembacaan
|
||||||
|
- ✅ Dot markers untuk setiap data point
|
||||||
|
- ✅ Auto-scaling untuk sumbu Y
|
||||||
|
|
||||||
|
## Data Management
|
||||||
|
- **Polling Interval**: Menggunakan `POLL_INTERVAL_MS` dari config
|
||||||
|
- **Max Data Points**: 20 (data lama akan dihapus otomatis)
|
||||||
|
- **Data Source**: `/sterilisasi/manual/last` endpoint
|
||||||
|
- **Update Strategy**: Push new data, slide old data
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
Grafik mengikuti tema dark mode aplikasi dengan:
|
||||||
|
- Background: Card background color
|
||||||
|
- Grid lines: Border color dengan opacity
|
||||||
|
- Labels: Muted color untuk teks
|
||||||
|
- Chart lines: Fire (suhu) dan Accent (tekanan) colors
|
||||||
|
|
||||||
|
## Cara Kerja
|
||||||
|
1. Setiap polling, data suhu dan tekanan baru ditambahkan ke array
|
||||||
|
2. Label waktu dibuat berdasarkan counter data point
|
||||||
|
3. Jika data melebihi 20 point, data terlama akan dihapus
|
||||||
|
4. Chart otomatis re-render dengan data terbaru
|
||||||
|
5. Bezier curve membuat garis lebih smooth
|
||||||
|
|
||||||
|
## File yang Dimodifikasi
|
||||||
|
1. `src/screens/ManualControlScreen.tsx`
|
||||||
|
- Import LineChart dari react-native-chart-kit
|
||||||
|
- State management untuk data grafik
|
||||||
|
- Logic untuk mengumpulkan data history
|
||||||
|
- Render grafik suhu dan tekanan
|
||||||
|
|
||||||
|
2. `src/styles/ManualControlScreen.styles.ts`
|
||||||
|
- Style untuk chart card
|
||||||
|
- Style untuk chart header
|
||||||
|
- Style untuk chart section
|
||||||
|
- Style untuk chart label
|
||||||
|
|
||||||
|
3. `package.json`
|
||||||
|
- Dependency: react-native-chart-kit
|
||||||
|
- Dependency: react-native-svg
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
Untuk menguji fitur:
|
||||||
|
1. Jalankan aplikasi dan buka layar Kontrol Manual
|
||||||
|
2. Pastikan data suhu dan tekanan masuk dari backend
|
||||||
|
3. Perhatikan grafik yang update secara real-time
|
||||||
|
4. Periksa bahwa maksimal 20 data point ditampilkan
|
||||||
|
5. Scroll untuk melihat kedua grafik (suhu dan tekanan)
|
||||||
|
|
||||||
|
## Catatan Pengembangan
|
||||||
|
- Grafik hanya menyimpan data selama sesi aktif
|
||||||
|
- Data grafik tidak disimpan ke database
|
||||||
|
- Jika keluar dari screen, data grafik akan di-reset
|
||||||
|
- Polling menggunakan interval yang sama dengan monitor real-time
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||||
|
ruby ">= 2.6.10"
|
||||||
|
|
||||||
|
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
|
||||||
|
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||||
|
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||||
|
gem 'xcodeproj', '< 1.26.0'
|
||||||
|
gem 'concurrent-ruby', '< 1.3.4'
|
||||||
|
|
||||||
|
# Ruby 3.4.0 has removed some libraries from the standard library.
|
||||||
|
gem 'bigdecimal'
|
||||||
|
gem 'logger'
|
||||||
|
gem 'benchmark'
|
||||||
|
gem 'mutex_m'
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
|
||||||
|
|
||||||
|
## Step 1: Start Metro
|
||||||
|
|
||||||
|
First, you will need to run **Metro**, the JavaScript build tool for React Native.
|
||||||
|
|
||||||
|
To start the Metro dev server, run the following command from the root of your React Native project:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using npm
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# OR using Yarn
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Build and run your app
|
||||||
|
|
||||||
|
With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app:
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using npm
|
||||||
|
npm run android
|
||||||
|
|
||||||
|
# OR using Yarn
|
||||||
|
yarn android
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps).
|
||||||
|
|
||||||
|
The first time you create a new project, run the Ruby bundler to install CocoaPods itself:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, and every time you update your native dependencies, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bundle exec pod install
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using npm
|
||||||
|
npm run ios
|
||||||
|
|
||||||
|
# OR using Yarn
|
||||||
|
yarn ios
|
||||||
|
```
|
||||||
|
|
||||||
|
If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device.
|
||||||
|
|
||||||
|
This is one way to run your app — you can also build it directly from Android Studio or Xcode.
|
||||||
|
|
||||||
|
## Step 3: Modify your app
|
||||||
|
|
||||||
|
Now that you have successfully run the app, let's make changes!
|
||||||
|
|
||||||
|
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh).
|
||||||
|
|
||||||
|
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
|
||||||
|
|
||||||
|
- **Android**: Press the <kbd>R</kbd> key twice or select **"Reload"** from the **Dev Menu**, accessed via <kbd>Ctrl</kbd> + <kbd>M</kbd> (Windows/Linux) or <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> (macOS).
|
||||||
|
- **iOS**: Press <kbd>R</kbd> in iOS Simulator.
|
||||||
|
|
||||||
|
## Congratulations! :tada:
|
||||||
|
|
||||||
|
You've successfully run and modified your React Native App. :partying_face:
|
||||||
|
|
||||||
|
### Now what?
|
||||||
|
|
||||||
|
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
|
||||||
|
- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
|
||||||
|
|
||||||
|
# Learn More
|
||||||
|
|
||||||
|
To learn more about React Native, take a look at the following resources:
|
||||||
|
|
||||||
|
- [React Native Website](https://reactnative.dev) - learn more about React Native.
|
||||||
|
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
|
||||||
|
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
|
||||||
|
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
|
||||||
|
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactTestRenderer from 'react-test-renderer';
|
||||||
|
import App from '../App';
|
||||||
|
|
||||||
|
test('renders correctly', async () => {
|
||||||
|
await ReactTestRenderer.act(() => {
|
||||||
|
ReactTestRenderer.create(<App />);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
apply plugin: "com.android.application"
|
||||||
|
apply plugin: "org.jetbrains.kotlin.android"
|
||||||
|
apply plugin: "com.facebook.react"
|
||||||
|
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||||
|
/**
|
||||||
|
* This is the configuration block to customize your React Native Android app.
|
||||||
|
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||||
|
*/
|
||||||
|
react {
|
||||||
|
/* Folders */
|
||||||
|
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||||
|
// root = file("../../")
|
||||||
|
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||||
|
// reactNativeDir = file("../../node_modules/react-native")
|
||||||
|
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||||
|
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||||
|
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
|
||||||
|
// cliFile = file("../../node_modules/react-native/cli.js")
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
// The list of variants to that are debuggable. For those we're going to
|
||||||
|
// skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized".
|
||||||
|
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||||
|
// debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"]
|
||||||
|
|
||||||
|
/* Bundling */
|
||||||
|
// A list containing the node command and its flags. Default is just 'node'.
|
||||||
|
// nodeExecutableAndArgs = ["node"]
|
||||||
|
//
|
||||||
|
// The command to run when bundling. By default is 'bundle'
|
||||||
|
// bundleCommand = "ram-bundle"
|
||||||
|
//
|
||||||
|
// The path to the CLI configuration file. Default is empty.
|
||||||
|
// bundleConfig = file(../rn-cli.config.js)
|
||||||
|
//
|
||||||
|
// The name of the generated asset file containing your JS bundle
|
||||||
|
// bundleAssetName = "MyApplication.android.bundle"
|
||||||
|
//
|
||||||
|
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||||
|
// entryFile = file("../js/MyApplication.android.js")
|
||||||
|
//
|
||||||
|
// A list of extra flags to pass to the 'bundle' commands.
|
||||||
|
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||||
|
// extraPackagerArgs = []
|
||||||
|
|
||||||
|
/* Hermes Commands */
|
||||||
|
// The hermes compiler command to run. By default it is 'hermesc'
|
||||||
|
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||||
|
//
|
||||||
|
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||||
|
// hermesFlags = ["-O", "-output-source-map"]
|
||||||
|
|
||||||
|
/* Autolinking */
|
||||||
|
autolinkLibrariesWithApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||||
|
*/
|
||||||
|
def enableProguardInReleaseBuilds = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preferred build flavor of JavaScriptCore (JSC)
|
||||||
|
*
|
||||||
|
* For example, to use the international variant, you can use:
|
||||||
|
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
|
||||||
|
*
|
||||||
|
* The international variant includes ICU i18n library and necessary data
|
||||||
|
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||||
|
* give correct results when using with locales other than en-US. Note that
|
||||||
|
* this variant is about 6MiB larger per architecture than default.
|
||||||
|
*/
|
||||||
|
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||||
|
|
||||||
|
android {
|
||||||
|
ndkVersion rootProject.ext.ndkVersion
|
||||||
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
|
namespace "com.sigasti"
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.sigasti"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
signingConfigs {
|
||||||
|
debug {
|
||||||
|
storeFile file('debug.keystore')
|
||||||
|
storePassword 'android'
|
||||||
|
keyAlias 'androiddebugkey'
|
||||||
|
keyPassword 'android'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
// Caution! In production, you need to generate your own keystore file.
|
||||||
|
// see https://reactnative.dev/docs/signed-apk-android.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
minifyEnabled enableProguardInReleaseBuilds
|
||||||
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// The version of react-native is set by the React Native Gradle Plugin
|
||||||
|
implementation("com.facebook.react:react-android")
|
||||||
|
|
||||||
|
if (hermesEnabled.toBoolean()) {
|
||||||
|
implementation("com.facebook.react:hermes-android")
|
||||||
|
} else {
|
||||||
|
implementation jscFlavor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# Notifee
|
||||||
|
-keep class app.notifee.** { *; }
|
||||||
|
-keep class io.invertase.notifee.** { *; }
|
||||||
|
-keepclassmembers class io.invertase.notifee.** { *; }
|
||||||
|
-dontwarn io.invertase.notifee.**
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".MainApplication"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||||
|
android:supportsRtl="true">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.sigasti
|
||||||
|
|
||||||
|
import com.facebook.react.ReactActivity
|
||||||
|
import com.facebook.react.ReactActivityDelegate
|
||||||
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||||
|
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||||
|
|
||||||
|
class MainActivity : ReactActivity() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||||
|
* rendering of the component.
|
||||||
|
*/
|
||||||
|
override fun getMainComponentName(): String = "Sigasti"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||||
|
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||||
|
*/
|
||||||
|
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||||
|
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.sigasti
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.facebook.react.PackageList
|
||||||
|
import com.facebook.react.ReactApplication
|
||||||
|
import com.facebook.react.ReactHost
|
||||||
|
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||||
|
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||||
|
|
||||||
|
class MainApplication : Application(), ReactApplication {
|
||||||
|
|
||||||
|
override val reactHost: ReactHost by lazy {
|
||||||
|
getDefaultReactHost(
|
||||||
|
context = applicationContext,
|
||||||
|
packageList =
|
||||||
|
PackageList(this).packages.apply {
|
||||||
|
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||||
|
// add(MyReactNativePackage())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
loadReactNative(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||||
|
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||||
|
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||||
|
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
|
||||||
|
>
|
||||||
|
|
||||||
|
<selector>
|
||||||
|
<!--
|
||||||
|
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||||
|
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||||
|
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||||
|
|
||||||
|
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||||
|
|
||||||
|
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||||
|
-->
|
||||||
|
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||||
|
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||||
|
</selector>
|
||||||
|
|
||||||
|
</inset>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@color/splashscreen_bg"/>
|
||||||
|
<item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/ic_launcher"/>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="splashscreen_bg">#080C10</color>
|
||||||
|
<color name="app_bg">#080C10</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Baglog Care</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||||
|
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||||
|
<item name="android:statusBarColor">@color/splashscreen_bg</item>
|
||||||
|
<item name="android:navigationBarColor">@color/splashscreen_bg</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
buildscript {
|
||||||
|
ext {
|
||||||
|
buildToolsVersion = "36.0.0"
|
||||||
|
minSdkVersion = 24
|
||||||
|
compileSdkVersion = 36
|
||||||
|
targetSdkVersion = 36
|
||||||
|
ndkVersion = "27.1.12297006"
|
||||||
|
kotlinVersion = "2.1.20"
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle")
|
||||||
|
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "com.facebook.react.rootproject"
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
# Use this property to specify which architecture you want to build.
|
||||||
|
# You can also override it from the CLI using
|
||||||
|
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||||
|
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||||
|
|
||||||
|
# Use this property to enable support to the new architecture.
|
||||||
|
# This will allow you to use TurboModules and the Fabric render in
|
||||||
|
# your application. You should enable this flag either if you want
|
||||||
|
# to write custom TurboModules/Fabric components OR use libraries that
|
||||||
|
# are providing them.
|
||||||
|
newArchEnabled=true
|
||||||
|
|
||||||
|
# Use this property to enable or disable the Hermes JS engine.
|
||||||
|
# If set to false, you will be using JSC instead.
|
||||||
|
hermesEnabled=true
|
||||||
|
|
||||||
|
# Use this property to enable edge-to-edge display support.
|
||||||
|
# This allows your app to draw behind system bars for an immersive UI.
|
||||||
|
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||||
|
edgeToEdgeEnabled=false
|
||||||