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
|
||||