Front-end aplikasi

This commit is contained in:
ilham 2026-07-14 21:11:39 +07:00
commit 3bea2cf0c8
172 changed files with 25770 additions and 0 deletions

18
Back-End/.gitignore vendored Normal file
View File

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

View File

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

59
Back-End/README.md Normal file
View File

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

View File

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

View File

@ -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 };

View File

@ -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,
};

321
Back-End/mqtt/mqttClient.js Normal file
View File

@ -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(); }
});
});
},
};

1437
Back-End/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
Back-End/package.json Normal file
View File

@ -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"
}
}

7
Back-End/railway.toml Normal file
View File

@ -0,0 +1,7 @@
[build]
builder = "NIXPACKS"
[deploy]
startCommand = "node server.js"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10

View File

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

25
Back-End/server.js Normal file
View File

@ -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}`));

105
Back-End/test-batch-id.js Normal file
View File

@ -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');
})();

View File

@ -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();

2
Front-End/.bundle/config Normal file
View File

@ -0,0 +1,2 @@
BUNDLE_PATH: "vendor/bundle"
BUNDLE_FORCE_RUBY_PLATFORM: 1

1
Front-End/.env Normal file
View File

@ -0,0 +1 @@
BACKEND_URL=https://backend-sigasti-production-6cf6.up.railway.app

4
Front-End/.eslintrc.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: '@react-native',
};

75
Front-End/.gitignore vendored Normal file
View File

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

5
Front-End/.prettierrc.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
arrowParens: 'avoid',
singleQuote: true,
trailingComma: 'all',
};

2
Front-End/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,2 @@
{
}

View File

@ -0,0 +1 @@
{}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

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

View File

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

16
Front-End/Gemfile Normal file
View File

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

97
Front-End/README.md Normal file
View File

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

View File

@ -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 />);
});
});

View File

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

Binary file not shown.

View File

@ -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.**

View File

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

View File

@ -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)
}

View File

@ -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)
}
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

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

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Baglog Care</string>
</resources>

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More