Upload Tugas Akhir TKK

This commit is contained in:
Wizznu 2026-07-09 09:49:35 +07:00
parent b8cb7a3ba6
commit e1628ef4db
6848 changed files with 976828 additions and 5259 deletions

View File

@ -1,5 +1,9 @@
{
"chat.tools.terminal.autoApprove": {
"git init": true
}
"git init": true,
"adb": true,
"flutter": true,
"type": true
},
"cmake.sourceDirectory": "F:/irrigo/linux"
}

View File

@ -1,119 +0,0 @@
# 🐛 Bug Fixes & Improvements Report - ApsGo
Laporan lengkap bug yang ditemukan dan perbaikan yang telah dilakukan.
## 📊 Summary
- **Total Issues Found**: 9
- **Critical**: 4 ✅ Fixed
- **Medium**: 3 ✅ Fixed
- **Minor**: 2 ⚠️ Noted
- **New Features Added**: 3
---
## 🔴 CRITICAL BUGS (Fixed)
### Bug #1: Memory Leak - StreamSubscription Tidak Di-dispose
**Severity**: 🔴 Critical
**Lokasi**: `lib/screens/dashboard_page.dart`
**Deskripsi:**
- `_authService.authStateChanges.listen()` tidak pernah di-cancel
- Memory leak setiap navigation
**Fix Applied:**
```dart
class _DashboardPageState extends State<DashboardPage> {
StreamSubscription? _authSubscription;
@override
void dispose() {
_authSubscription?.cancel(); // ✅ Cleanup
super.dispose();
}
}
```
---
### Bug #2: Background Services Tidak Berhenti
**Severity**: 🔴 Critical
**Lokasi**: Singleton services
**Fix Applied:**
- Implement AppLifecycleListener di `main.dart`
- Services auto-stop ketika app paused/terminated
---
### Bug #3: Race Condition di Multi-Pot Watering
**Severity**: 🔴 Critical
**Solution**: Railway Worker dengan BullMQ (concurrency: 1)
---
### Bug #4: No Firebase Connection Check
**Severity**: 🔴 Critical
**Fix**: Created `ConnectionMonitorService`
---
## 🟡 MEDIUM BUGS (Fixed)
### Bug #5-7: Error Handling, Time Comparison, Magic Numbers
**Fixes:**
- Improved error handling dengan user feedback
- Proper time formatting di Railway Worker
- Created `automation_constants.dart` untuk centralized config
---
## 🟢 MINOR BUGS (Noted)
### Bug #8-9: WillPopScope Deprecated, No Input Validation
**Status**: Low priority, functionality works
---
## 🚀 NEW FEATURES
### 1. Railway Worker (Complete Solution)
- ✅ 24/7 automation bahkan saat HP mati
- ✅ Redis queue untuk reliable task management
- ✅ Production-grade architecture
### 2. Connection Monitoring
- ✅ Real-time Firebase status
- ✅ Better error messages
### 3. Constants & Validation
- ✅ Centralized configuration
- ✅ Validation helpers
---
## 📚 Documentation
1. ✅ `DEPLOYMENT_GUIDE.md` - Step-by-step Railway deployment
2. ✅ `railway-worker/README.md` - Worker documentation
3. ✅ This bug report
---
## 🎯 Result
**Before:** Not production-ready (memory leaks, race conditions, no offline support)
**After:** Production-ready dengan reliable 24/7 automation
**Next Step:** Deploy Railway Worker following DEPLOYMENT_GUIDE.md
---
**Last Updated:** February 10, 2026

View File

@ -1,243 +0,0 @@
# Laporan Perbaikan Bug ApsGo
**Tanggal**: 26 Januari 2026
**Status**: ✅ SELESAI
## Ringkasan Bug yang Diperbaiki
### 1. 🔒 Bug Logout Tiba-tiba ke Landing Page
**Masalah**:
- Aplikasi terkadang logout secara tiba-tiba saat klik menu Kontrol atau menu lainnya
- User ter-redirect ke Landing Page tanpa sengaja
**Penyebab**:
- Tidak ada monitoring auth state di DashboardPage
- Ketika Firebase Auth token expired atau ada perubahan auth state, tidak ada handler yang tepat
- Navigation stack tidak terlindungi dengan baik
**Solusi**:
✅ Tambahkan `authStateChanges` listener di `initState()` DashboardPage
```dart
@override
void initState() {
super.initState();
// Monitor auth state untuk mencegah logout tidak terduga
_authService.authStateChanges.listen((user) {
if (user == null && mounted) {
// User logged out, redirect to login
Navigator.of(context).pushNamedAndRemoveUntil(
'/login',
(route) => false,
);
}
});
}
```
**File yang Diubah**:
- `lib/screens/dashboard_page.dart`
---
### 2. 💧 Bug Pompa Tidak Langsung Aktif Saat Soil Moisture Rendah
**Masalah**:
- Pompa terkadang tidak langsung aktif ketika soil moisture di bawah ambang bawah
- Delay response terlalu lama (10 detik)
- Cooldown period terlalu lama (5 menit)
**Penyebab**:
- Timer check sensor berjalan setiap 10 detik → terlalu lambat
- Cooldown 5 menit antar penyiraman → terlalu lama untuk kondisi kritis
- Kurang logging untuk debugging
**Solusi**:
**Interval Check**: Ubah dari 10 detik → **5 detik** (lebih responsif)
```dart
_sensorCheckTimer = Timer.periodic(
const Duration(seconds: 5), // Sebelumnya: 10 detik
(timer) => _checkSensorThreshold(),
);
```
**Cooldown Period**: Kurangi dari 5 menit → **2 menit**
```dart
if (diff.inMinutes < 2) { // Sebelumnya: 5 menit
debugPrint('⏳ POT $potNumber: Cooldown active (${2 - diff.inMinutes} min remaining)');
return;
}
```
**Tambah Logging Detail**:
- Log setiap check sensor dengan nilai threshold
- Log setiap trigger penyiraman
- Log progress penyiraman per 5 detik
- Log saat target tercapai
**Pastikan Flag Reset**: Tambah delay sebelum reset flag untuk memastikan cleanup selesai
```dart
await Future.delayed(const Duration(milliseconds: 500));
_isWateringActive[potKey] = false;
```
**Error Handling**: Tambahkan try-catch untuk matikan pompa saat error
```dart
} catch (e) {
debugPrint('❌ Error watering pot by sensor: $e');
// Matikan semua untuk safety
try {
await _dbService.setPompaAir(false);
await _dbService.setPot(potNumber, false);
} catch (cleanupError) {
debugPrint('❌ Error during cleanup: $cleanupError');
}
_isWateringActive[potKey] = false;
}
```
**File yang Diubah**:
- `lib/services/kontrol_automation_service.dart`
---
### 3. 🔌 Verifikasi Mapping Soil Sensor ke Mosfet
**Masalah yang Diperiksa**:
- Apakah soil_1 bisa mempengaruhi mosvet_3 dst
- Apakah mapping soil_x ke mosvet_y sudah benar
**Hasil Verifikasi**:
✅ Mapping sudah **BENAR**:
```
soil_1 (Pot 1) → mosvet_3 (Valve Pot 1)
soil_2 (Pot 2) → mosvet_4 (Valve Pot 2)
soil_3 (Pot 3) → mosvet_5 (Valve Pot 3)
soil_4 (Pot 4) → mosvet_6 (Valve Pot 4)
soil_5 (Pot 5) → mosvet_7 (Valve Pot 5)
```
**Catatan**:
- mosvet_1 = Pompa Air
- mosvet_2 = Pompa Nutrisi
- mosvet_3 s/d mosvet_7 = Valve Pot 1-5
**Perbaikan**:
✅ Tambah komentar dan logging untuk memperjelas mapping
```dart
// pot 1 → mosvet_3, pot 2 → mosvet_4, ... pot 5 → mosvet_7
debugPrint('💧 Starting watering: POT $potNumber (mosvet_${potNumber + 2})');
```
✅ Tambah logging tracking nilai soil sensor
```dart
debugPrint('🌡️ soil_$i = $soilValue (threshold: $batasBawah)');
debugPrint('⚠️ $soilKey ($soilValue) < batasBawah ($batasBawah) Triggering watering for POT $i');
```
**File yang Diubah**:
- `lib/services/kontrol_automation_service.dart`
---
## Perubahan Detail
### File: `lib/screens/dashboard_page.dart`
**Perubahan**:
1. Import `AuthService`
2. Tambah instance `_authService`
3. Tambah `initState()` dengan auth state listener
### File: `lib/services/kontrol_automation_service.dart`
**Perubahan**:
1. ✅ Timer interval: 10s → 5s (line ~183)
2. ✅ Cooldown: 5 min → 2 min (line ~248)
3. ✅ Tambah logging detail di `_checkSensorThreshold()` (line ~217)
4. ✅ Tambah logging mapping pot (line ~254)
5. ✅ Tambah logging progress watering (line ~265)
6. ✅ Tambah delay sebelum reset flag (line ~283)
7. ✅ Improve error handling dengan cleanup (line ~286)
---
## Testing Checklist
Setelah update, pastikan test hal-hal berikut:
### Auth & Navigation
- [ ] Login dan navigate ke Dashboard
- [ ] Klik menu Kontrol → tidak logout
- [ ] Klik menu Histori → tidak logout
- [ ] Buka drawer dan klik menu → tidak logout
- [ ] Test dengan koneksi internet ON/OFF
- [ ] Test dengan token yang akan expired
### Sensor Automation
- [ ] Aktifkan mode Sensor
- [ ] Set soil moisture di bawah batas bawah (misal: 25%)
- [ ] Cek pompa aktif dalam **< 5 detik**
- [ ] Cek valve pot yang tepat aktif (soil_1 → mosvet_3, dst)
- [ ] Cek log di console untuk tracking
- [ ] Test dengan multiple pot di bawah threshold bersamaan
- [ ] Test cooldown period (2 menit)
### Edge Cases
- [ ] Test saat Firebase disconnect
- [ ] Test saat sensor data tidak valid (0, null, string)
- [ ] Test saat automation service crash
- [ ] Test multiple user logout/login
---
## Logging untuk Debugging
Untuk monitor aktivitas, cek console log dengan pattern:
**Sensor Mode**:
```
🌡️ Sensor Mode: Started
🌡️ Checking thresholds: batas_bawah=30, batas_atas=80
🌡️ soil_1 = 25 (threshold: 30)
⚠️ soil_1 (25) < batasBawah (30) Triggering watering for POT 1
💧 Starting watering: POT 1 (mosvet_3)
🌡️ Target: soil_1 should reach >= 80 (currently: 25)
💧 POT 1 watering: 5s, soil_1: 35
💧 POT 1 watering: 10s, soil_1: 50
✅ POT 1 reached target: 80 >= 80
✅ Sensor Mode: Watering completed for POT 1 (15s)
```
**Cooldown**:
```
⏳ POT 1: Cooldown active (1 min remaining)
```
**Errors**:
```
❌ Error watering pot by sensor: [error message]
❌ Error during cleanup: [error message]
```
---
## Rekomendasi Lanjutan
1. **Tambah UI Indicator**: Tampilkan status automation (active/inactive) di Dashboard
2. **Notification**: Push notification saat pompa aktif otomatis
3. **History Log**: Simpan log automation ke Firebase untuk review
4. **Manual Override**: Button untuk force stop automation
5. **Sensor Calibration**: UI untuk kalibrasi sensor soil moisture
---
## Kontak
Jika masih ada masalah setelah update ini, catat:
1. Timestamp kejadian
2. Screenshot console log
3. Nilai sensor saat kejadian
4. Action yang dilakukan user
Happy Gardening! 🌱

View File

@ -1,99 +0,0 @@
# 🧪 **DEBUG MODE TESTING GUIDE**
## **Langkah Testing untuk Diagnosa Log Issue**
### **STEP 1: Test Flutter App dengan Debug Console**
1. **Buka Flutter App di Debug Mode**
```bash
cd f:\ApsGo\ApsGo
flutter run --debug
```
2. **Lihat Debug Console Output**
- Buka VS Code Debug Console atau Terminal
- Perhatikan log output saat save configuration
### **STEP 2: Test Waktu Mode Setting**
1. **Di Flutter App:**
- Buka **Kontrol** → **Waktu Mode**
- Set jadwal 2-3 menit dari sekarang
- **PASTIKAN toggle "Waktu Mode" AKTIF**
- Set durasi: 10 detik
- Klik **SIMPAN**
2. **Yang Harus Muncul di Console:**
```
🔧 [DEBUG] Starting save configuration...
✅ [DEBUG] Local storage saved
📊 [DEBUG] Config to Firebase: {waktu_1: 14:30, waktu_2: 18:00, durasi_1: 10, durasi_2: 600, waktu: true}
🔥 [DEBUG] Updating Firebase kontrol config: ...
✅ [DEBUG] Firebase config saved successfully
🚀 [DEBUG] Starting Waktu Mode automation...
✅ [DEBUG] Waktu Mode started
```
### **STEP 3: Cek Railway Logs**
1. **Buka Railway Dashboard**
- Pergi ke: https://railway.app/dashboard
- Pilih project: **myreppril**
- Klik **Deploy Logs**
2. **Yang Harus Muncul Setiap 30 Detik:**
```
⏰ [DEBUG] Checking scheduled watering... 2026-02-10T...
📊 [DEBUG] Retrieved kontrol config: {
"waktu_1": "14:30",
"waktu_2": "18:00",
"durasi_1": 10,
"durasi_2": 600,
"waktu": true
}
🕐 [DEBUG] Current time: 14:28, Date: 2026-02-10
📅 [DEBUG] Scheduled times - Jadwal 1: 14:30, Jadwal 2: 18:00
⏰ [DEBUG] Jadwal 1 time mismatch - Expected: 14:30, Current: 14:28
```
3. **Saat Jadwal Trigger (di waktu yang tepat):**
```
🕐 JADWAL 1 TRIGGERED: 14:30
📌 Added to queue: jadwal_1_2026-02-10_14:30
💧 Processing Job: jadwal_1_2026-02-10_14:30
Type: waktu_jadwal_1
Pots: [1, 2, 3, 4, 5]
Duration: 10s
🔛 Turning ON: mosvet_1, mosvet_2, mosvet_3, mosvet_4...
```
---
## 🚨 **KEMUNGKINAN MASALAH & SOLUSI**
### **Jika Flutter Debug Log Tidak Muncul:**
- ❌ **Firebase connection issue**
- ✅ **Cek internet connection**
- ✅ **Restart app dengan `flutter clean && flutter run`**
### **Jika Railway Log Tidak Update:**
- ❌ **Environment variables salah**
- ❌ **Firebase project tidak match**
- ✅ **Re-deploy Railway worker**
### **Jika Config Tersimpan tapi Worker Tidak Jalan:**
- ❌ **Toggle "Waktu Mode" tidak aktif**
- ❌ **Jadwal time format salah**
- ✅ **Pastikan format waktu "HH:mm" (contoh: "14:30")**
---
## 📱 **QUICK FIX CHECKLIST**
- [ ] Toggle "Waktu Mode" sudah **AKTIF**
- [ ] Format waktu sudah benar (HH:mm)
- [ ] Flutter app berjalan dalam debug mode
- [ ] Railway worker masih running (cek status online)
- [ ] Firebase project sama (project-ta-951b4)
- [ ] Internet connection stabil

View File

@ -1,479 +0,0 @@
# 🚀 Railway Deployment Guide - ApsGo Worker
Panduan lengkap untuk deploy Railway Worker ke cloud Railway.app dan mengintegrasikannya dengan aplikasi Flutter ApsGo.
## 📋 Prerequisites
Sebelum memulai, pastikan Anda sudah punya:
1. ✅ Akun Railway.app (gratis) - [Daftar di sini](https://railway.app)
2. ✅ Firebase project dengan Realtime Database
3. ✅ Git installed di komputer
4. ✅ Node.js installed (v18+) untuk testing lokal (optional)
## 📁 Struktur Project
```
ApsGo/
├── lib/ # Flutter app
├── android/
├── railway-worker/ # ← Worker yang akan di-deploy
│ ├── worker.js # Main worker code
│ ├── package.json
│ ├── railway.json # Railway config
│ ├── .env.example # Environment variables template
│ └── README.md
└── README.md
```
---
## 🔧 STEP 1: Setup Firebase Service Account
Worker butuh Firebase Admin SDK untuk akses database. Ikuti langkah berikut:
### 1.1. Download Service Account Key
1. Buka [Firebase Console](https://console.firebase.google.com)
2. Pilih project ApsGo Anda
3. Klik ⚙️ **Project Settings** (di sidebar kiri)
4. Tab **Service Accounts**
5. Klik **Generate New Private Key**
6. Download file JSON (jangan share file ini ke siapapun!)
### 1.2. Extract Credentials dari JSON
Buka file JSON yang di-download, cari 3 informasi ini:
```json
{
"type": "service_account",
"project_id": "your-project-id-123", // ← SIMPAN INI
"private_key_id": "...",
"private_key": "-----BEGIN PRIVATE KEY-----\n....\n-----END PRIVATE KEY-----\n", // ← SIMPAN INI
"client_email": "firebase-adminsdk-xxxxx@your-project-id.iam.gserviceaccount.com", // ← SIMPAN INI
...
}
```
Simpan 3 nilai ini, akan digunakan di Step 3.
### 1.3. Get Firebase Database URL
Format: `https://YOUR-PROJECT-ID-default-rtdb.firebaseio.com`
Cek di Firebase Console → Realtime Database → Copy URL di bagian atas.
---
## 🚂 STEP 2: Setup Railway Project
### 2.1. Login ke Railway
1. Buka [railway.app](https://railway.app)
2. Klik **Login** → Login dengan GitHub (recommended)
3. Authorize Railway untuk akses GitHub Anda
### 2.2. Create New Project
1. Klik **New Project**
2. Pilih **Deploy from GitHub repo**
3. Jika belum connect GitHub:
- Klik **Configure GitHub App**
- Authorize Railway
- Pilih repository **ApsGo**
4. Railway akan scan repository Anda
### 2.3. Setup Worker Service
1. Railway detect root project (Flutter), kita perlu custom path
2. Klik **Settings** (di sidebar kiri service)
3. Scroll ke **Build & Deploy**
4. Set **Root Directory**: `railway-worker`
5. **Build Command**: `npm install`
6. **Start Command**: `npm start`
7. Klik **Save Changes**
### 2.4. Add Redis Service
Worker butuh Redis untuk queue system:
1. Klik **New** (di sidebar project)
2. Pilih **Database** → **Add Redis**
3. Railway akan auto-provision Redis
4. Redis akan otomatis terhubung ke worker (melalui private network)
---
## 🔐 STEP 3: Configure Environment Variables
### 3.1. Add Variables di Railway
1. Klik service **worker** (bukan Redis)
2. Klik tab **Variables**
3. Tambahkan variables berikut:
| Variable Name | Value | Cara Isi |
|--------------|--------|----------|
| `FIREBASE_PROJECT_ID` | your-project-id-123 | Dari Step 1.2 |
| `FIREBASE_CLIENT_EMAIL` | firebase-adminsdk-xxx@... | Dari Step 1.2 |
| `FIREBASE_PRIVATE_KEY` | "-----BEGIN PRIVATE KEY..." | Copy SELURUH private_key dari JSON, pastikan ada quotes |
| `FIREBASE_DATABASE_URL` | https://xxx.firebaseio.com | Dari Step 1.3 |
### 3.2. Redis Variables (Auto)
Railway akan auto-inject Redis variables:
- `REDIS_HOST`: `redis.railway.internal` (auto)
- `REDIS_PORT`: `6379` (auto)
- `REDIS_PASSWORD`: (auto-generated)
Atau Railway bisa provide single variable:
- `REDIS_URL`: `redis://default:password@redis.railway.internal:6379`
Worker code sudah handle both formats.
### 3.3. IMPORTANT: Format Private Key
Private key HARUS include newlines (`\n`). Contoh:
```
"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBg...\n...akhir key...\n-----END PRIVATE KEY-----\n"
```
**Jika error "Invalid key":**
1. Pastikan ada quotes di awal dan akhir
2. Pastikan ada `\n` (bukan enter sesungguhnya)
3. Copy paste langsung dari JSON file
---
## 🚀 STEP 4: Deploy!
### 4.1. Trigger Deployment
Setelah environment variables di-set:
1. Railway akan auto-deploy
2. Atau klik **Deploy** → **Redeploy**
### 4.2. Monitor Deployment
1. Klik tab **Deployments**
2. Lihat build logs:
- ✅ `npm install` berhasil
- ✅ `npm start` running
- ✅ Firebase initialized
- ✅ Redis connected
3. Jika error, check **Logs** tab
### 4.3. Check Worker Logs
Setelah deploy sukses, check logs:
```
🚀 Starting ApsGo Railway Worker...
📡 Firebase Project: your-project-id
📦 Redis: redis.railway.internal:6379
✅ Firebase Admin initialized
✅ Redis connected
✅ Waktu Mode scheduler started (check every 30s)
✅ Sensor Mode monitoring started
✅ Auto history logging started (every 10 minutes)
✅ History cleanup scheduled (daily at 2 AM)
✨ ApsGo Railway Worker is running!
🎯 Worker is ready to process jobs...
```
Jika lihat log seperti ini, **SUKSES!**
---
## 🧪 STEP 5: Testing Worker
### 5.1. Test Waktu Mode
1. Buka Flutter app ApsGo
2. Masuk ke **Kontrol** → **Waktu Mode**
3. Set jadwal 1 menit dari sekarang (misal: sekarang 14:05, set 14:06)
4. Set durasi: 10 detik
5. Save configuration
**Di Railway Logs, tunggu 1 menit:**
```
🕐 JADWAL 1 TRIGGERED: 14:06
📌 Added to queue: jadwal_1_2026-02-10_14:06
💧 Processing Job: jadwal_1_2026-02-10_14:06
Type: waktu_jadwal_1
Pots: [1, 2, 3, 4, 5]
Duration: 10s
🔛 Turning ON: mosvet_1, mosvet_2, mosvet_3, mosvet_4...
⏳ 10s remaining...
🔴 Turning OFF
📊 History logged: 2026-02-10 14:06
✅ Job completed successfully
```
**Check di Flutter app:**
- Pompa dan valve nyala selama 10 detik
- Histori tercatat di halaman Histori
### 5.2. Test Sensor Mode
1. Buka **Kontrol** → **Sensor Mode**
2. Set batas_bawah: 50%
3. Enable otomatis
4. Set durasi_sensor: 15 detik
**Simulasi sensor:**
- Update Firebase Realtime DB manual:
- Path: `/data/soil_1`
- Value: `30` (di bawah 50%)
**Di Railway Logs:**
```
🌡️ SENSOR TRIGGERED: POT 1
Soil moisture: 30% < 50%
Mode: fixed, Duration: 15s
📌 Added to queue: sensor-pot-1-1707562800000
💧 Processing Job: sensor-pot-1-1707562800000
Type: sensor_threshold
Pots: [1]
Duration: 15s
🔛 Turning ON: mosvet_1, mosvet_3
⏳ 15s remaining...
🔴 Turning OFF
✅ Job completed successfully
```
### 5.3. Test Auto History Logging
**Check setiap 10 menit:**
```
📊 Auto-logged sensor data: 14:10
📊 Auto-logged sensor data: 14:20
📊 Auto-logged sensor data: 14:30
```
Verify di Firebase Console → Realtime Database → `/history`
---
## 📊 STEP 6: Monitoring & Maintenance
### 6.1. Health Check
Worker auto health check setiap 5 menit:
```
💚 HEALTH CHECK:
Firebase: ✅ Connected
Redis: ✅ Connected
Queue: 0 active, 0 waiting
```
### 6.2. View Metrics di Railway
1. Klik service **worker**
2. Tab **Metrics**:
- CPU Usage
- Memory Usage
- Network Traffic
3. Tab **Logs**:
- Real-time logs
- Filter by time/keyword
### 6.3. Setup Alerts (Optional)
1. Klik **Settings** → **Notifications**
2. Connect Slack/Discord/Email
3. Get notified jika service down
### 6.4. Restart Worker
**Manual Restart:**
1. Tab **Deployments**
2. Klik **...** (three dots)
3. **Redeploy**
**Auto Restart:**
- Railway auto-restart jika crash (configured in `railway.json`)
---
## 💰 STEP 7: Billing & Cost Management
### 7.1. Railway Free Tier
**Limits:**
- $5 credit per month (gratis)
- Cukup untuk small IoT project
- Auto-sleep jika idle (configurable)
**Typical Usage (ApsGo Worker):**
- Worker: ~$2-3/month
- Redis: ~$1-2/month
- **Total**: ~$3-5/month (masih dalam free tier!)
### 7.2. Upgrade to Hobby Plan (Optional)
Jika butuh lebih:
- $5/month
- More resources
- No sleep
- Priority support
---
## 🔧 TROUBLESHOOTING
### Problem: "Firebase initialization failed"
**Solution:**
1. Check `FIREBASE_PRIVATE_KEY` format
2. Pastikan ada quotes dan `\n`
3. Verify project ID benar
4. Check Firebase rules allow admin access
### Problem: "Redis connection error"
**Solution:**
1. Pastikan Redis service aktif di Railway
2. Check Redis variables auto-injected
3. Restart worker service
### Problem: "Worker tidak trigger jadwal"
**Solution:**
1. Check timezone: Worker use UTC
- Convert waktu lokal ke UTC
- Atau set `TZ` env variable: `Asia/Jakarta`
2. Check Firebase `/kontrol/waktu` = `true`
3. Check logs untuk error
### Problem: "Memory leak / High CPU"
**Solution:**
1. Check logs untuk infinite loop
2. Verify cooldown working
3. Check Redis queue size: `await queue.getJobCounts()`
### Problem: "Too many Firebase reads"
**Solution:**
1. Worker optimize untuk minimize reads
2. Check sensor mode `otomatis` tidak stuck ON
3. Consider increase check interval di code
---
## 🔄 STEP 8: Update Worker Code
### 8.1. Update via Git
1. Edit code di `railway-worker/worker.js`
2. Commit & push ke GitHub:
```bash
git add .
git commit -m "Update worker logic"
git push
```
3. Railway auto-detect push → auto-deploy
### 8.2. Rollback Deployment
Jika ada bug setelah update:
1. Tab **Deployments**
2. Pilih deployment sebelumnya yang sukses
3. Klik **...** → **Redeploy**
---
## 📱 STEP 9: Integrate dengan Flutter App
### 9.1. Update App Logic
**PENTING:** Sekarang scheduling di-handle oleh Railway Worker, bukan Flutter app.
**Rekomendasi perubahan:**
1. **Disable** local automation services ketika deploy ke production
2. Keep local services hanya untuk **testing/development**
3. Tambahkan indicator di UI: "Server-side automation active"
### 9.2. Add Status Indicator (Optional)
Tambahkan di Flutter app untuk show worker status:
```dart
// Check worker last activity
final historyRef = FirebaseDatabase.instance.ref('history');
final lastLog = await historyRef.limitToLast(1).once();
if (lastLog.snapshot.value != null) {
// Worker active (logged dalam 10 menit terakhir)
showWorkerActiveIcon();
}
```
---
## 🎉 STEP 10: Done!
Congratulations! Worker Anda sekarang berjalan 24/7 di cloud.
**Apa yang terjadi sekarang:**
- ✅ Jadwal waktu berjalan otomatis (meskipun HP mati)
- ✅ Sensor monitoring aktif terus
- ✅ History auto-logged setiap 10 menit
- ✅ Old history auto-cleanup setiap hari
- ✅ Reliable, scalable, production-ready
**Next Steps:**
1. Monitor logs selama 24 jam pertama
2. Fine-tune configuration (durasi, threshold, dll)
3. Setup alerts untuk critical errors
4. Consider backup strategy untuk history data
---
## 📚 Additional Resources
- [Railway Documentation](https://docs.railway.app)
- [BullMQ Documentation](https://docs.bullmq.io)
- [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup)
- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices)
## 🆘 Need Help?
- Railway Discord: [discord.gg/railway](https://discord.gg/railway)
- Firebase Support: [firebase.google.com/support](https://firebase.google.com/support)
- ApsGo Issues: Create issue di GitHub repository
---
## 📝 Checklist Deployment
Copy checklist ini untuk reference:
- [ ] Firebase Service Account downloaded
- [ ] Railway account created
- [ ] GitHub repository connected
- [ ] Redis service added
- [ ] Environment variables configured
- [ ] Worker deployed successfully
- [ ] Logs showing "Worker is running"
- [ ] Waktu Mode tested
- [ ] Sensor Mode tested
- [ ] Auto logging verified
- [ ] Monitoring setup
- [ ] Flutter app updated (optional)
- [ ] Documentation completed
---
**🎊 Happy Automating! Selamat menggunakan ApsGo dengan Railway Worker!**

View File

@ -1,131 +0,0 @@
# 🚀 Cara Deploy Optimasi Firebase ke Railway
## Perubahan yang Dibuat
### Masalah Sebelumnya
- SDK timeout setiap 60 detik (delay 10s per check)
- REST API fallback berfungsi tapi lama
- Tidak ada smart caching/skipping untuk SDK yang gagal
### Solusi yang Diimplementasikan
1. ⚡ **Timeout lebih cepat**: 10s → 5s (SDK) & 8s (REST)
2. 🧠 **Smart fallback**: Skip SDK setelah 3x gagal berturut
3. 📊 **API Statistics**: Track SDK vs REST usage
4. 🔧 **Konsistensi**: Semua fungsi pakai timeout yang sama
## Cara Deploy ke Railway
### Opsi 1: Git Push (Recommended)
```bash
# 1. Add & commit changes
git add railway-worker/worker.js
git commit -m "fix: Optimize Firebase connection with smart fallback"
# 2. Push ke repository
git push origin main
# 3. Railway akan auto-deploy (jika connected)
```
### Opsi 2: Manual Deploy via Railway CLI
```bash
# 1. Install Railway CLI (jika belum)
npm install -g @railway/cli
# 2. Login ke Railway
railway login
# 3. Link project (jika belum)
railway link
# 4. Deploy
railway up
```
### Opsi 3: Deploy via Railway Dashboard
1. Buka [Railway Dashboard](https://railway.app/dashboard)
2. Pilih project **ApsGo Worker**
3. Klik **Deployments** tab
4. Klik **Deploy Now** atau **Redeploy**
5. Tunggu deployment selesai (~2-3 menit)
## Verifikasi Setelah Deploy
Setelah deploy, cek logs di Railway:
### Log yang NORMAL:
```
✅ [SMART] Skipping SDK (3+ consecutive failures), using REST API directly...
✅ [DEBUG] REST API successful!
📊 API Stats: SDK=0 | REST=15 | Errors=3
```
### Log yang OPTIMAL:
```
✅ [DEBUG] Attempting SDK fetch...
✅ [DEBUG] REST API successful!
📊 API Stats: SDK=25 | REST=5 | Errors=0
```
### Indikator Performa:
- **Sebelum**: Delay ~10s setiap check
- **Sesudah**: Delay ~0-5s setiap check
- **Speed up**: 2-10x lebih cepat!
## Troubleshooting
### Jika masih ada warning "SDK fetch failed"
**NORMAL** - Ini bukan error, sistem bekerja dengan baik via REST API
### Jika "Both SDK and REST API failed"
**MASALAH** - Check:
1. Firebase credentials masih valid?
2. Network Railway ke Firebase OK?
3. Firebase Database Rules allow?
### Jika "SMART: Resetting SDK retry counter"
**BAGUS** - Sistem mencoba reconnect SDK setelah 50 menit
## Monitoring
Untuk monitoring jangka panjang, perhatikan:
1. **API Stats ratio**:
- SDK=0, REST=100 → SDK tidak berfungsi sama sekali
- SDK=70, REST=30 → SDK kadang timeout (normal)
- SDK=95, REST=5 → SDK sangat baik!
2. **Consecutive Errors**:
- 0-2: Normal fluctuation
- 3: Smart fallback activated
- >10: Ada masalah serius dengan SDK
## Tips Tambahan
### Jika ingin SDK lebih sering retry:
Edit line di `worker.js`:
```javascript
const SKIP_SDK_THRESHOLD = 3; // Ubah ke 5 atau 10
const RESET_THRESHOLD_AFTER = 50; // Ubah ke 20 (20 menit)
```
### Jika ingin REST API sebagai default:
```javascript
const SKIP_SDK_THRESHOLD = 0; // Langsung skip SDK
```
## Status Setelah Optimasi
- ✅ SDK timeout lebih cepat (10s → 5s)
- ✅ Smart fallback implemented
- ✅ API statistics tracking
- ✅ Consistent timeout across all functions
- ✅ Better error handling
- ✅ Performance improved 2-10x
---
**Catatan**: Perubahan ini **backward compatible** - tidak mengubah functionality, hanya optimasi performa.

View File

@ -1,210 +0,0 @@
# 🔍 ANALISIS MASALAH PENJADWALAN
## ❌ MASALAH TERIDENTIFIKASI
### 1. **Railway Worker TIDAK TERINSTALL DI RAILWAY**
Dari screenshot Railway Anda sebelumnya:
- **Source Repo:** `awisnuu/myreppril`
- **Branch:** `main`
- **Root Directory:** TIDAK DI-SET
**PROBLEM:** Railway mencoba build dari root folder, tapi kode worker ada di subfolder `railway-worker/`
### 2. **File package.json Tidak Di-Push ke GitHub**
Railway membutuhkan:
- ✅ `worker.js`
- ✅ `package.json`
- ✅ `railway.json` atau root directory config
Kalau file tidak ada di GitHub repo, Railway tidak bisa build.
---
## ✅ YANG SUDAH BENAR
### Flutter Side (Sudah Oke ✓)
```dart
// waktu_config_page.dart line 578-583
await _dbService.updateKontrolConfig({
'waktu_1': _jadwalPenyiraman[0]['jamMulai'], // Format: "07:30"
'waktu_2': _jadwalPenyiraman[1]['jamMulai'], // Format: "17:00"
'durasi_1': durasi1Detik, // Dalam detik
'durasi_2': durasi2Detik, // Dalam detik
'waktu': _isWaktuModeActive, // true/false
});
```
**Path Firebase:** `/kontrol/`
- ✅ `waktu` = true/false (toggle mode)
- ✅ `waktu_1` = "07:30" (string HH:mm)
- ✅ `waktu_2` = "17:00" (string HH:mm)
- ✅ `durasi_1` = 60 (integer seconds)
- ✅ `durasi_2` = 60 (integer seconds)
### Worker Side (Sudah Oke ✓)
```javascript
// worker.js line 175-204
const kontrolConfig = snapshot.val();
if (!kontrolConfig || !kontrolConfig.waktu) return;
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
// Check Jadwal 1
if (kontrolConfig.waktu_1 && kontrolConfig.waktu_1 === currentTime) {
// Trigger penyiraman
}
```
**Worker membaca path yang SAMA:** `/kontrol/`
- ✅ Format waktu cocok: "HH:mm"
- ✅ Check interval: 30 detik
- ✅ Debouncing: Prevent double trigger
---
## 🚨 ROOT CAUSE
**Railway worker TIDAK JALAN** karena:
### Issue #1: Root Directory Tidak Di-Set
Railway tidak tahu folder mana yang harus di-build.
**Solution:** Set Root Directory di Railway Settings:
```
railway-worker
```
### Issue #2: File Belum Di-Push ke GitHub
Worker files mungkin hanya ada di local, belum di-commit/push.
**Solution:**
```bash
git add railway-worker/
git commit -m "Add Railway worker for 24/7 scheduling"
git push origin main
```
### Issue #3: Environment Variables Belum Di-Set
Railway butuh Firebase credentials.
**Solution:** Add di Railway Variables:
- `FIREBASE_PROJECT_ID`
- `FIREBASE_CLIENT_EMAIL`
- `FIREBASE_PRIVATE_KEY`
- `FIREBASE_DATABASE_URL`
---
## 🎯 FORMAT DATA YANG BENAR
### Firebase `/kontrol/` Structure:
```json
{
"kontrol": {
"waktu": true, // ← Mode waktu ON/OFF
"waktu_1": "07:30", // ← Jadwal 1 (HH:mm string)
"waktu_2": "17:00", // ← Jadwal 2 (HH:mm string)
"durasi_1": 60, // ← Durasi 1 (seconds integer)
"durasi_2": 60, // ← Durasi 2 (seconds integer)
"otomatis": false, // ← Mode sensor ON/OFF
"batas_bawah": 40,
"batas_atas": 100
}
}
```
### Worker Logic:
```javascript
// Cek setiap 30 detik
if (kontrolConfig.waktu === true) {
const now = new Date();
const currentTime = "07:30"; // Format sama dengan waktu_1/waktu_2
if (kontrolConfig.waktu_1 === currentTime) {
// TRIGGER PENYIRAMAN
}
}
```
**✅ Format sudah MATCH antara Flutter dan Worker!**
---
## 🔧 LANGKAH PERBAIKAN
### Step 1: Push Worker Files ke GitHub
```bash
cd f:\ApsGo\ApsGo
git status
git add railway-worker/
git commit -m "Add Railway worker for 24/7 automation"
git push origin main
```
### Step 2: Set Root Directory di Railway
1. Railway Dashboard → Select service `myreppril`
2. Settings tab
3. Cari "Root Directory" atau "Build" section
4. Set: `railway-worker`
5. Save
### Step 3: Add Environment Variables
Di Railway Settings → Variables:
```
FIREBASE_PROJECT_ID=project-ta-a119e
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@project-ta-a119e.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----
FIREBASE_DATABASE_URL=https://project-ta-a119e-default-rtdb.firebaseio.com
```
### Step 4: Redeploy
Railway akan auto-redeploy setelah:
- Root directory di-set
- Environment variables ditambahkan
### Step 5: Monitor Logs
Railway Logs → Harus muncul:
```
🚀 Starting ApsGo Railway Worker...
✅ Firebase Admin initialized
✅ Redis connected
✅ Waktu Mode scheduler started (check every 30s)
```
---
## 🧪 TESTING
### Test Manual:
1. Set jadwal di APK (misalnya 2 menit dari sekarang)
2. Pastikan `/kontrol/waktu` = true di Firebase
3. Tunggu 2 menit
4. Cek Railway logs → Harus ada:
```
🕐 JADWAL 1 TRIGGERED: 07:30
📌 Added to queue: jadwal_1
💧 Processing Job...
```
---
## 📊 KESIMPULAN
### ✅ TIDAK ADA MASALAH DI:
- [x] Flutter code (sudah benar menulis ke Firebase)
- [x] Worker code (sudah benar membaca dari Firebase)
- [x] Format data (sudah compatible)
- [x] Path Firebase (sama-sama `/kontrol/`)
### ❌ MASALAH DI:
- [ ] Railway deployment (worker belum jalan)
- [ ] Root directory belum di-set
- [ ] Environment variables mungkin belum lengkap
### 🎯 ACTION REQUIRED:
1. Push railway-worker ke GitHub
2. Set root directory di Railway
3. Verify environment variables
4. Redeploy & monitor logs

View File

@ -1,466 +0,0 @@
# 🎉 Implementasi Dual Mode System & Bug Fixes
**Tanggal**: 26 Januari 2026
**Status**: ✅ COMPLETED
---
## 🚀 Fitur Baru: Dual Mode System
### Mode 1: Smart Mode (Adaptif) 🧠
**Konsep:**
- Pompa ON: ketika `soil < Batas Minimal`
- Pompa OFF: ketika `soil >= Batas Maksimal` ATAU timeout (durasi habis)
- Prioritas: **Mencapai target** > durasi
**Cara Kerja:**
```
Trigger: soil_1 = 25% < 30% (batas minimal)
├─ Pompa Air ON
├─ Valve POT 1 ON
├─ Loop setiap 5 detik:
│ ├─ Check soil_1 value
│ ├─ Jika soil_1 >= 80% → STOP (target tercapai) ✅
│ └─ Jika waktu >= durasi (misal 60s) → STOP (timeout) ⏱️
└─ Pompa & Valve OFF
```
**Log Example:**
```
🧠 Smart Mode: Target soil_1 >= 80 (currently: 25), max 60s
💧 POT 1 watering: 5s, soil_1: 35
💧 POT 1 watering: 10s, soil_1: 55
💧 POT 1 watering: 15s, soil_1: 82
✅ POT 1 reached target: 82 >= 80 (Smart Mode)
✅ Sensor Mode: Watering completed for POT 1 (15s, mode: smart)
```
**Use Case:**
- Tanaman sensitif terhadap over-watering
- Ingin efisiensi air maksimal
- Sensor soil moisture berfungsi baik
---
### Mode 2: Fixed Duration (Durasi Tetap) ⏱️
**Konsep:**
- Pompa ON: ketika `soil < Batas Minimal`
- Pompa OFF: setelah **tepat durasi yang ditentukan**
- Prioritas: **Durasi tetap** (tidak peduli sensor)
**Cara Kerja:**
```
Trigger: soil_1 = 25% < 30% (batas minimal)
├─ Pompa Air ON
├─ Valve POT 1 ON
├─ Loop setiap 5 detik (monitoring only):
│ ├─ Log soil_1 value (tidak break)
│ └─ Continue sampai durasi habis
└─ Setelah durasi (misal 30s) → Pompa & Valve OFF
```
**Log Example:**
```
⏱️ Fixed Duration Mode: Watering for exactly 30s
💧 POT 1 watering: 5s/30s, soil_1: 40
💧 POT 1 watering: 10s/30s, soil_1: 60
💧 POT 1 watering: 15s/30s, soil_1: 78
💧 POT 1 watering: 20s/30s, soil_1: 88
💧 POT 1 watering: 25s/30s, soil_1: 92
💧 POT 1 watering: 30s/30s, soil_1: 95
✅ POT 1 fixed duration completed: 30s
✅ Sensor Mode: Watering completed for POT 1 (30s, mode: fixed)
```
**Use Case:**
- Sensor tidak akurat atau rusak
- Ingin penyiraman konsisten setiap kali
- Sudah tahu berapa lama perlu menyiram
---
## 🎨 UI Changes
### Sensor Config Page
**Tambahan Baru:**
```
┌─────────────────────────────────────────────┐
│ Mode Penyiraman │
├─────────────────────────────────────────────┤
│ ● Smart Mode (Adaptif) │
│ Siram hingga batas atas tercapai atau │
│ timeout │
├─────────────────────────────────────────────┤
│ ○ Fixed Duration (Durasi Tetap) │
│ Siram selama durasi yang ditentukan │
└─────────────────────────────────────────────┘
Batas Minimal (%): 30
Batas Maksimal (%): 80
Durasi Penyiraman: 10 detik
Smart Mode: Pompa akan mati otomatis saat
mencapai batas atas atau durasi habis
sebagai safety timeout
Fixed Duration: Pompa akan menyiram selama
durasi yang ditentukan, tidak peduli nilai
sensor
```
**Interaksi:**
- Radio buttons untuk pilih mode
- Info text berubah dinamis sesuai mode
- Durasi memiliki fungsi berbeda per mode:
- Smart: Max timeout (safety)
- Fixed: Durasi aktual penyiraman
---
## 🔧 Technical Implementation
### 1. Data Model Changes
**Local Storage (sensor_config):**
```dart
{
'batasMinimal': '30',
'batasMaksimal': '80',
'durasi': '10',
'durasiUnit': 'detik',
'mode': 'smart', // ← NEW: 'smart' or 'fixed'
}
```
**Firebase (kontrol):**
```json
{
"batas_atas": 80,
"batas_bawah": 30,
"durasi_sensor": 10, // ← NEW: dalam detik
"mode_sensor": "smart", // ← NEW: 'smart' or 'fixed'
"otomatis": true
}
```
### 2. Code Changes
**File Modified:**
1. ✅ `lib/screens/sensor_config_page.dart`
- Tambah radio buttons untuk mode selection
- Update `_sensorConfig` dengan field `mode`
- Convert durasi ke seconds sebelum save ke Firebase
- Dynamic info text berdasarkan mode
2. ✅ `lib/services/kontrol_automation_service.dart`
- Update `_checkSensorThreshold()` untuk ambil `mode_sensor` dan `durasi_sensor`
- Update `_waterPotBySensor()` dengan parameter `mode` dan `durasiSeconds`
- Implementasi dual logic dengan if-else:
- Smart mode: break jika `soil >= batasAtas`
- Fixed mode: continue sampai durasi habis
3. ✅ `lib/services/firebase_database_service.dart`
- Tidak perlu perubahan, sudah support `updateKontrolConfig()`
### 3. Logic Flow
**Smart Mode:**
```dart
if (mode == 'smart') {
while (elapsedSeconds < durasiSeconds && _isSensorModeActive) {
await Future.delayed(Duration(seconds: 5));
elapsedSeconds += 5;
currentSoil = await getSensorData();
if (currentSoil >= batasAtas) {
break; // ✅ Target tercapai
}
}
if (elapsedSeconds >= durasiSeconds) {
// ⏰ Timeout (safety)
}
}
```
**Fixed Mode:**
```dart
if (mode == 'fixed') {
while (elapsedSeconds < durasiSeconds && _isSensorModeActive) {
await Future.delayed(Duration(seconds: 5));
elapsedSeconds += 5;
currentSoil = await getSensorData(); // Log only, tidak break
}
// ✅ Durasi selesai
}
```
---
## 🐛 Bug Fixes
### Bug #1: Logout Tiba-tiba ke Login Page ✅ FIXED
**Root Cause:**
- Auth state listener trigger saat first load
- Listener tidak memeriksa apakah user sudah login sebelumnya
- Bisa menyebabkan loop redirect
**Solution:**
```dart
class _DashboardPageState extends State<DashboardPage> {
bool _isInitialized = false;
@override
void initState() {
super.initState();
// Verify user on init
if (_authService.currentUser == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
Navigator.of(context).pushNamedAndRemoveUntil(
'/login',
(route) => false,
);
}
});
return;
}
_isInitialized = true;
// Listener hanya trigger jika sudah initialized
_authService.authStateChanges.listen((user) {
if (_isInitialized && user == null && mounted) {
Navigator.of(context).pushNamedAndRemoveUntil(
'/login',
(route) => false,
);
}
});
}
}
```
**Key Changes:**
1. ✅ Tambah flag `_isInitialized`
2. ✅ Check `currentUser` di `initState()`
3. ✅ Listener hanya trigger setelah initialized
4. ✅ Always check `mounted` sebelum navigate
### Bug #2: Crash yang Menyebabkan Logout
**Root Cause:**
- Unhandled exceptions di Firebase calls
- setState() dipanggil setelah widget disposed
- StreamBuilder error tidak di-handle
**Solution:**
```dart
// Always wrap await dengan try-catch
try {
final data = await _dbService.getSensorData();
if (mounted) { // ← Check mounted
setState(() {
// Update state
});
}
} catch (e) {
debugPrint('Error: $e');
if (mounted) {
// Show error tapi tidak crash
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
```
**Files Enhanced with Error Handling:**
1. ✅ `dashboard_page.dart` - StreamBuilder error state
2. ✅ `kontrol_page.dart` - Load Firebase state
3. ✅ `sensor_config_page.dart` - Load config
4. ✅ `waktu_config_page.dart` - Load config
5. ✅ `pot_selection_page.dart` - Load mode status
---
## 📊 Comparison Table
| Feature | Smart Mode 🧠 | Fixed Duration ⏱️ |
|---------|---------------|-------------------|
| **Stop Condition** | Target OR timeout | Durasi habis |
| **Sensor Dependency** | ✅ High | ❌ Low |
| **Water Efficiency** | ✅ Optimal | ⚠️ Depends |
| **Predictability** | ⚠️ Variable | ✅ Consistent |
| **If Sensor Fails** | ⚠️ Use timeout | ✅ Still works |
| **Over-watering Risk** | ❌ Low | ⚠️ Possible |
| **Best For** | Smart irrigation | Manual control |
---
## 🧪 Testing Checklist
### Dual Mode Testing
**Smart Mode:**
- [ ] Trigger penyiraman (soil < 30%)
- [ ] Pompa mati saat mencapai target (soil >= 80%)
- [ ] Pompa mati saat timeout (durasi habis)
- [ ] Log menunjukkan "Smart Mode" dengan target
- [ ] Cooldown 2 menit bekerja
**Fixed Duration:**
- [ ] Trigger penyiraman (soil < 30%)
- [ ] Pompa menyiram selama durasi penuh
- [ ] Pompa TIDAK mati meskipun target tercapai
- [ ] Log menunjukkan "Fixed Duration Mode"
- [ ] Progress counter: 5s/30s, 10s/30s, dst
**Edge Cases:**
- [ ] Switch mode saat automation aktif
- [ ] Durasi 0 atau negatif (validation)
- [ ] Multiple pot trigger bersamaan
- [ ] Sensor return invalid value (null, "", 0)
- [ ] Firebase disconnected saat watering
### Bug Fixes Testing
**Auth & Navigation:**
- [ ] Login → Dashboard (tidak logout)
- [ ] Dashboard → Kontrol → tidak logout
- [ ] Dashboard → Histori → tidak logout
- [ ] Drawer navigation → tidak logout
- [ ] Back button → tidak logout
- [ ] Network ON/OFF → tidak logout
- [ ] Multiple tab/window → tidak logout
**Error Handling:**
- [ ] Firebase call error → show snackbar, tidak crash
- [ ] setState after dispose → tidak crash
- [ ] StreamBuilder error → tampil error state
- [ ] Invalid sensor data → handle gracefully
---
## 📝 Migration Guide
### Existing Users
**Jika sudah ada konfigurasi lama:**
1. App akan auto-migrate ke format baru
2. Default mode: `smart`
3. Default durasi unit: `detik`
4. Konfigurasi lama tetap tersimpan
**Jika belum ada konfigurasi:**
1. Default values:
- Batas Minimal: 30%
- Batas Maksimal: 80%
- Durasi: 10 detik
- Mode: Smart
### Firebase Update
**Manual Steps (Optional):**
```json
// Di Firebase Console, tambahkan fields baru:
{
"kontrol": {
"durasi_sensor": 30, // Default 30 detik
"mode_sensor": "smart" // Default smart mode
}
}
```
**Auto Update:**
- Fields akan otomatis ter-create saat user save config pertama kali
---
## 🔮 Future Enhancements
1. **Hybrid Mode**: Kombinasi smart + fixed (min & max duration)
2. **Per-Pot Mode**: Setiap pot bisa punya mode berbeda
3. **Schedule Mode**: Smart mode di pagi, fixed mode di sore
4. **Learning Mode**: AI adjust durasi berdasarkan history
5. **Notification**: Push notif saat watering complete
6. **History Graph**: Chart durasi vs hasil kelembaban
---
## 📞 Support
**Jika ada masalah:**
1. **Check Console Log:**
```
🧠 Smart Mode: Target ...
⏱️ Fixed Duration Mode: ...
✅ Watering completed ...
❌ Error: ...
```
2. **Verify Firebase:**
- `kontrol/mode_sensor` exist?
- `kontrol/durasi_sensor` valid number?
- `kontrol/otomatis` = true?
3. **Reset Config:**
- Delete local storage
- Delete Firebase `/kontrol` node
- Restart app
- Setup ulang
---
## 🎯 Summary
### ✅ Completed
1. **Dual Mode System**
- Smart Mode (adaptif dengan sensor)
- Fixed Duration (durasi tetap)
- UI untuk selection
- Backend logic implementation
2. **Bug Fixes**
- Fix logout tiba-tiba
- Enhanced error handling
- Prevent crashes
- Safe navigation
3. **Code Quality**
- Better logging
- Comprehensive error handling
- Clean separation of modes
- Well documented
### 📈 Improvements
**Performance:**
- Check interval: 10s → 5s (lebih responsif)
- Cooldown: 5 min → 2 min (lebih cepat)
**Reliability:**
- Auth state management fixed
- Error boundaries added
- Graceful degradation
**User Experience:**
- Clear mode selection
- Dynamic info text
- Better feedback
- Predictable behavior
---
**Happy Gardening! 🌱**

View File

@ -1,131 +0,0 @@
# ✅ Complete Firebase Warning Suppression
## 🎯 Current Status
Your worker is **WORKING PERFECTLY**! ✅
- Worker tidak crash
- Firebase connected
- Redis connected
- Schedule checks berjalan
**Firebase warnings yang muncul adalah WARNING SAJA**, sistem tetap berjalan normal.
---
## 🔕 Recommended: Add Environment Variable
Untuk **COMPLETELY suppress** Firebase warnings di Railway:
### Step 1: Add Variable di Railway
1. Railway Dashboard → Service **myreppril**
2. Tab **Variables**
3. Click **New Variable**
4. Add:
```
Name: NODE_ENV
Value: production
```
5. Click **Add**
Railway akan auto-redeploy.
### Step 2: Verify Results
Setelah redeploy, logs akan **CLEAN**:
```
✅ Firebase Admin initialized
✅ Waktu Mode scheduler started (check every 60s)
✅ Sensor Mode monitoring started
🔌 Firebase realtime connection active
💚 HEALTH CHECK:
Firebase: ✅ Connected
Redis: ✅ Connected
💓 Heartbeat: Worker alive for 0h 1m
```
**NO MORE RED WARNINGS!** 🎉
---
## 🤔 Apakah Warnings Berbahaya?
**TIDAK!** Firebase warnings ini:
### ✅ AMAN - Tidak Berbahaya
- Internal SDK warnings
- Deprecated API notices
- Long-polling connection info
- **System tetap berfungsi normal**
### ❌ TIDAK Mempengaruhi:
- Scheduling functionality
- Firebase connections
- Data reads/writes
- Worker stability
### 📊 Proof System Working:
```
✅ Firebase: Connected
✅ Redis: Connected
✅ Queue: 0 active, 0 waiting
✅ Waktu Mode scheduler: Running
✅ Sensor Mode: Running
✅ Health checks: Passing
```
---
## 🎯 Kesimpulan
### Current State (With Warnings):
- ✅ Worker **RUNNING** 24/7
- ✅ All connections **ACTIVE**
- ✅ Schedules will **TRIGGER** correctly
- ⚠️ Cosmetic warnings (dapat diabaikan)
### After Adding NODE_ENV (Recommended):
- ✅ Worker **RUNNING** 24/7
- ✅ All connections **ACTIVE**
- ✅ Schedules will **TRIGGER** correctly
- ✅ **CLEAN LOGS** - no warnings!
---
## 🚀 Testing Schedule
Untuk verify system bekerja:
1. **Set jadwal di Flutter app:**
- Waktu: **5-10 menit dari sekarang**
- Enable Mode Waktu
- Simpan
2. **Monitor Railway logs saat waktu tiba:**
```
🕐 JADWAL 1 TRIGGERED: HH:MM
📌 Added to queue: jadwal_1_...
✅ Worker completed job jadwal_1_...
```
3. **Check ESP32/Hardware:**
- Pompa harus nyala sesuai durasi
- Valve harus buka sesuai POT
**Jika semua ini terjadi = SYSTEM WORKING PERFECTLY!** ✅
---
## 💡 Recommendation
**Option A: Ignore Warnings (Easiest)**
- System sudah berfungsi
- Warnings tidak berbahaya
- No action needed
**Option B: Suppress Warnings (Cleanest)**
- Add `NODE_ENV=production` di Railway
- Logs akan lebih bersih
- Same functionality
**Pilihan Anda!** Both options are valid. Sistema tetap bekerja dengan atau tanpa warnings.

View File

@ -1,237 +0,0 @@
# Full History System Implementation
## 📋 Overview
Sistem history lengkap yang mencatat dan menampilkan data sensor dari Firebase secara otomatis.
## 🔧 Komponen
### 1. **History Logging Service**
File: `lib/services/history_logging_service.dart`
**Fungsi:**
- Auto-save data sensor setiap **10 menit**
- Persistent - berjalan otomatis saat app aktif
- Cleanup otomatis data lama (>30 hari)
**Key Features:**
```dart
- interval: Duration(minutes: 10)
- Auto start saat Firebase initialized
- isActive property untuk check status
```
### 2. **Firebase Database Service Updates**
File: `lib/services/firebase_database_service.dart`
**Method Baru:**
- `saveHistory()` - Simpan snapshot data sensor
- `getHistoryByDateRange()` - Load data berdasarkan range tanggal
- `getLatestHistory()` - Load data terbaru (100 entries)
- `clearOldHistory()` - Hapus data >30 hari
### 3. **History Page Lengkap**
File: `lib/screens/histori_page.dart`
**Fitur:**
- ✅ Load data real dari Firebase
- ✅ Filter by date range
- ✅ Filter per pot atau semua pot
- ✅ Hitung rata-rata otomatis
- ✅ Refresh manual dengan IconButton
- ✅ Loading states
- ✅ Error handling
- ✅ Empty state info
- ✅ Auto load last 7 days by default
### 4. **Main App Integration**
File: `lib/main.dart`
**Changes:**
- Import HistoryLoggingService
- Start service saat Firebase initialized
- Service berjalan global di background
## 📊 Struktur Data Firebase
```
/history
/2024-12-28
/14:30
{
suhu: 28.5,
kelembapan: 65.2,
ldr: 820,
soil_1: 45.2,
soil_2: 52.1,
soil_3: 48.7,
soil_4: 50.3,
soil_5: 46.9,
timestamp: 1703761800000
}
/14:40
{...}
```
## 🎯 Flow Data
1. **Auto Logging (Background)**
```
App Start → Firebase Init → HistoryLoggingService.start()
→ Timer (10 min) → saveHistory() → Firebase /history
```
2. **Manual Load (UI)**
```
User Opens Histori Page → Load Default (7 days)
→ getHistoryByDateRange() → Calculate Averages → Display
```
3. **Filter & Refresh**
```
User Selects Date Range → _loadHistoryData()
→ Display Updated Data
User Clicks Refresh → _loadHistoryData() → Display
```
## 📱 UI Features
### Overall Averages
- Suhu Rata-Rata
- Kelembaban Udara Rata-Rata
- LDR Rata-Rata
### Per Pot Averages
- Expandable cards untuk setiap pot
- Detail: Suhu, Kelembaban, Soil Moisture, Light
### Filter Options
- Date Range Picker (calendar UI)
- Pot Selector (dropdown)
- Semua Pot
- Pot 1
- Pot 2
- Pot 3
- Pot 4
- Pot 5
## 🚀 How to Use
### For Users
1. Buka tab "Histori"
2. Data akan otomatis dimuat (7 hari terakhir)
3. Klik calendar icon untuk pilih date range
4. Pilih pot dari dropdown untuk filter
5. Klik refresh icon untuk reload data
### For Developers
```dart
// Start logging service
final loggingService = HistoryLoggingService();
loggingService.start();
// Stop logging service (optional)
loggingService.stop();
// Check if active
if (loggingService.isActive) {
print('Service is running');
}
// Get history data
final dbService = FirebaseDatabaseService();
final history = await dbService.getHistoryByDateRange(
DateTime.now().subtract(Duration(days: 7)),
DateTime.now(),
);
```
## ⚙️ Configuration
### Logging Interval
Edit `lib/services/history_logging_service.dart`:
```dart
static const Duration interval = Duration(minutes: 10); // Change here
```
### Data Retention
Edit `lib/services/firebase_database_service.dart`:
```dart
await clearOldHistory(30); // Keep last 30 days
```
## 🔍 Testing Checklist
- [ ] Service start otomatis saat app launch
- [ ] Data tersimpan setiap 10 menit
- [ ] History page load data dari Firebase
- [ ] Filter date range berfungsi
- [ ] Filter per pot berfungsi
- [ ] Calculate averages akurat
- [ ] Refresh button berfungsi
- [ ] Loading states tampil
- [ ] Error handling works
- [ ] Empty state tampil jika no data
## ⚠️ Important Notes
1. **First Time Use**: Data history akan mulai tersimpan setelah 10 menit pertama. Sebelum itu, akan menampilkan data saat ini.
2. **Firebase Rules**: Pastikan Firebase Realtime Database rules allow read/write untuk /history:
```json
{
"rules": {
"history": {
".read": "auth != null",
".write": "auth != null"
}
}
}
```
3. **Performance**: Cleanup otomatis data >30 hari untuk menjaga performa database.
4. **Memory**: Service menggunakan Timer, pastikan stop() dipanggil jika tidak dibutuhkan lagi (optional, karena global service).
## 📈 Future Enhancements
Possible improvements:
- Export data to CSV
- Chart/graph visualization
- Notification untuk anomali data
- Configurable logging interval dari UI
- Data comparison antar pot
- Weekly/Monthly summary reports
## 🐛 Troubleshooting
**Problem**: Data tidak tersimpan
- Check Firebase connection
- Check auth status
- Check console logs untuk error message
**Problem**: History page kosong
- Tunggu 10 menit untuk data pertama
- Check Firebase rules
- Check internet connection
**Problem**: Service tidak start
- Check Firebase initialization
- Check console logs
- Restart app
## 📝 Change Log
### v1.0 - Full History System
- ✅ Auto-logging service (10 min interval)
- ✅ Complete history page with Firebase integration
- ✅ Date range filter
- ✅ Per pot filtering
- ✅ Automatic averages calculation
- ✅ Cleanup old data (>30 days)
- ✅ Loading & error states
- ✅ Manual refresh option
---
**Status**: ✅ FULLY IMPLEMENTED
**Last Updated**: December 2024

View File

@ -1,423 +0,0 @@
# 📦 IMPLEMENTASI SELESAI - ApsGo Production Ready
## ✅ SEMUA SELESAI DIKERJAKAN!
Tanggal: 10 Februari 2026
---
## 📂 File-file Baru yang Dibuat
### 1. Railway Worker (Backend Service)
```
railway-worker/
├── worker.js ✅ Complete worker implementation
├── package.json ✅ Dependencies & scripts
├── railway.json ✅ Railway deployment config
├── .env.example ✅ Environment variables template
├── .gitignore ✅ Git ignore rules
└── README.md ✅ Worker documentation
```
### 2. Flutter Services (Bug Fixes & New)
```
lib/services/
├── automation_constants.dart ✅ NEW - Centralized constants
├── connection_monitor_service.dart ✅ NEW - Connection monitoring
├── kontrol_automation_service.dart ✅ UPDATED - Fixed & improved
├── history_logging_service.dart ✅ UPDATED - Use constants
├── firebase_database_service.dart (existing, no changes)
└── auth_service.dart (existing, no changes)
```
### 3. Flutter Core (Bug Fixes)
```
lib/
├── main.dart ✅ UPDATED - AppLifecycle observer
└── screens/
└── dashboard_page.dart ✅ UPDATED - Fixed memory leak
```
### 4. Documentation
```
├── DEPLOYMENT_GUIDE.md ✅ Step-by-step Railway deployment (4600+ words)
├── BUGS_AND_FIXES.md ✅ Bug report & fixes summary
├── RAILWAY_QUICK_START.md ✅ Quick reference guide
└── BUG_FIXES_REPORT.md (existing file in repo)
```
---
## 🐛 Bug yang Diperbaiki
### Critical (4 bugs)
1. ✅ **Memory Leak** - StreamSubscription disposal
2. ✅ **Background Services** - AppLifecycle management
3. ✅ **Race Condition** - Railway Worker dengan BullMQ queue
4. ✅ **No Connection Check** - ConnectionMonitorService
### Medium (3 bugs)
5. ✅ **Error Handling** - Improved dengan user feedback
6. ✅ **Time Comparison** - Proper formatting di worker
7. ✅ **Magic Numbers** - Centralized constants
### Minor (2 bugs)
8. ⚠️ **WillPopScope Deprecated** - Noted (still works)
9. ⚠️ **Input Validation** - Helpers created (UI integration pending)
---
## 🚀 Fitur Baru
### 1. Railway Worker (24/7 Backend)
**Teknologi:**
- Node.js 18+
- Firebase Admin SDK
- BullMQ (job queue)
- Redis (in-memory DB)
- Cron (scheduled tasks)
**Kemampuan:**
- ✅ Waktu Mode - Schedule penyiraman by time
- ✅ Sensor Mode - Auto watering by threshold
- ✅ Auto History Logging - Every 10 minutes
- ✅ Auto Cleanup - Daily at 2 AM (retain 30 days)
- ✅ Health Monitoring - Every 5 minutes
- ✅ Graceful Shutdown - Clean resource cleanup
- ✅ Error Recovery - Auto-retry & safety turn-off
**Keuntungan:**
- 🌟 Berjalan 24/7 meskipun HP mati
- 🌟 Reliable (Railway auto-restart jika crash)
- 🌟 Scalable (bisa handle multiple users/devices)
- 🌟 Cost-effective ($3-5/month, free tier available)
- 🌟 Production-grade architecture
### 2. Connection Monitoring
**Features:**
- Real-time Firebase connection status
- Stream untuk listen connection changes
- Wait-for-connection utility
- Callbacks untuk custom handling
**Usage:**
```dart
final monitor = ConnectionMonitorService();
monitor.start();
if (monitor.isConnected) {
// Safe to proceed with Firebase operations
}
monitor.connectionStream.listen((connected) {
print(connected ? 'Connected' : 'Disconnected');
});
```
### 3. Automation Constants
**Features:**
- Centralized configuration values
- Validation helpers
- Self-documenting code
- Easy maintenance
**Examples:**
```dart
AutomationConstants.defaultDurasiDetik // 60
AutomationConstants.wateringCooldownSeconds // 120
AutomationConstants.totalPots // 5
AutomationConstants.isValidDurasi(30) // true
```
---
## 📊 Perbandingan Sebelum vs Sesudah
### Sebelum
- ❌ Scheduling hanya jalan saat app buka
- ❌ Memory leak di dashboard
- ❌ Background services tidak berhenti
- ❌ Race condition di multi-pot watering
- ❌ No connection check (silent failures)
- ❌ Magic numbers everywhere
- ❌ Poor error handling
- ❌ Not production-ready
### Sesudah
- ✅ Scheduling 24/7 dengan Railway Worker
- ✅ Clean memory management
- ✅ Proper lifecycle handling
- ✅ Redis queue prevent race conditions
- ✅ Connection monitoring & better errors
- ✅ Centralized constants
- ✅ Improved error handling & user feedback
- ✅ **PRODUCTION-READY!**
---
## 🏗️ Arsitektur Sistem
### Old Architecture (Flutter Only)
```
┌─────────────┐
│ Flutter App │ ← Timer/Stream (hanya saat app buka)
└──────┬──────┘
┌──────────────┐
│ Firebase RTDB│
└──────┬───────┘
┌──────────────┐
│ ESP32/Sensor │
└──────────────┘
❌ Problem: App tertutup = automation stop
```
### New Architecture (Production)
```
┌─────────────┐
│ Flutter App │ ← UI & Manual Control
└──────┬──────┘
┌──────────────────────────────────┐
│ Firebase Realtime DB │ ← Central Data Hub
└─────────┬────────────────────────┘
↓ ↓
┌─────────────────┐ ┌──────────────┐
│ Railway Worker │ │ ESP32/Sensor │
│ (24/7 Cloud) │ └──────────────┘
│ │
│ • Scheduler │
│ • Automation │
│ • History Log │
│ │
│ ↓ Redis Queue│
└─────────────────┘
✅ Solution: Worker always running, independent dari app
```
---
## 💰 Cost Estimate
### Railway Free Tier
- **Credit**: $5/month (gratis)
- **Worker**: ~$2-3/month
- **Redis**: ~$1-2/month
- **Total**: ~$3-5/month
- **Verdict**: Masuk free tier! 🎉
### Railway Hobby Plan (Optional)
- **Price**: $5/month
- **Benefits**:
- More resources
- No auto-sleep
- Priority support
- Better for production
### Recommendation
Start dengan **Free Tier**, upgrade ke Hobby jika needed.
---
## 📖 Dokumentasi yang Tersedia
### 1. DEPLOYMENT_GUIDE.md (LENGKAP!)
**Sections:**
- ✅ Prerequisites checklist
- ✅ Step 1: Firebase Service Account setup
- ✅ Step 2: Railway project creation
- ✅ Step 3: Environment variables
- ✅ Step 4: Deploy process
- ✅ Step 5: Testing (Waktu & Sensor mode)
- ✅ Step 6: Monitoring & maintenance
- ✅ Step 7: Billing & cost management
- ✅ Step 8: Troubleshooting (common issues)
- ✅ Step 9: Update worker code
- ✅ Step 10: Flutter app integration
- ✅ Deployment checklist
**4600+ words**, super detail, screenshot-ready!
### 2. BUGS_AND_FIXES.md
- List 9 bugs found
- Severity classification
- Code examples before/after
- Impact analysis
- Files changed
### 3. RAILWAY_QUICK_START.md
- TL;DR version untuk quick reference
- 5-step deployment ringkas
- Troubleshooting ringkas
- Cost summary
### 4. railway-worker/README.md
- Worker architecture
- Features explanation
- Local development setup
- Environment variables
- How it works (Waktu & Sensor mode)
- Safety features
- Monitoring guide
- Troubleshooting
---
## 🧪 Testing Checklist
### Before Production Deployment
- [ ] Test waktu mode locally (Firebase Emulator optional)
- [ ] Test sensor mode with manual threshold change
- [ ] Verify Railway deployment successful
- [ ] Check logs show "Worker is running"
- [ ] Test jadwal 1 trigger
- [ ] Test jadwal 2 trigger
- [ ] Test sensor threshold trigger
- [ ] Verify auto history logging (wait 10 min)
- [ ] Verify Firebase RTDB data updated correctly
- [ ] Test connection loss scenario
- [ ] Monitor for 24 hours (stability test)
### Production Monitoring (First Week)
- [ ] Check Railway logs daily
- [ ] Monitor Firebase reads/writes usage
- [ ] Monitor Redis memory usage
- [ ] Verify schedules executing on time
- [ ] Check sensor mode responsiveness
- [ ] Monitor app performance (no memory issues)
- [ ] User feedback (if any issues)
---
## 🚀 Langkah Deploy (Summary)
### Quick Deploy (10 menit)
1. **Firebase**: Download service account key
2. **Railway**: Create project, connect GitHub repo
3. **Redis**: Add Redis database di Railway
4. **Config**: Set environment variables di Railway
5. **Deploy**: Railway auto-deploy
6. **Test**: Check logs & test dari Flutter app
**Detail:** Lihat `DEPLOYMENT_GUIDE.md`
---
## 🔧 Maintenance Guide
### Daily
- ✅ Auto: Worker health check (every 5 min)
- ✅ Auto: History logging (every 10 min)
### Weekly
- Check Railway logs untuk errors
- Monitor cost usage
- Verify schedules running correctly
### Monthly
- Review Firebase RTDB size
- ✅ Auto: History cleanup (daily, retain 30 days)
- Check Railway invoice
### As Needed
- Update worker code (git push → auto-deploy)
- Adjust automation parameters (batas, durasi, dll)
- Scale up if needed (upgrade Railway plan)
---
## ⚠️ Known Limitations & TODOs
### Current Limitations
1. Worker timezone = UTC (need to convert dari local time)
2. Flutter app masih punya local automation (should disable di production)
3. No push notifications untuk alerts (future enhancement)
### TODO (Nice to Have)
- [ ] Disable local automation di production build
- [ ] Add UI indicator "Server automation active"
- [ ] Push notifications untuk alerts (FCM)
- [ ] Unit tests untuk automation logic
- [ ] Integration tests end-to-end
- [ ] Replace WillPopScope dengan PopScope
- [ ] Input validation di UI forms
- [ ] Error reporting (Sentry/Crashlytics)
---
## 📞 Support & Resources
### Documentation
- ✅ DEPLOYMENT_GUIDE.md
- ✅ BUGS_AND_FIXES.md
- ✅ RAILWAY_QUICK_START.md
- ✅ railway-worker/README.md
### External Resources
- [Railway Docs](https://docs.railway.app)
- [BullMQ Docs](https://docs.bullmq.io)
- [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup)
- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices)
### Community
- Railway Discord: discord.gg/railway
- Firebase Support: firebase.google.com/support
---
## 🎉 Kesimpulan
### Achievement Unlocked! 🏆
**✅ Semua yang diminta telah selesai:**
1. ✅ **Code lengkap Railway Worker**
- worker.js (600+ lines)
- Full-featured dengan queue, monitoring, auto-cleanup
- Production-ready
2. ✅ **Fix semua bug yang ditemukan**
- 4 critical bugs fixed
- 3 medium bugs fixed
- 2 minor bugs noted
- Memory management improved
- Error handling improved
3. ✅ **Step-by-step deployment guide**
- DEPLOYMENT_GUIDE.md (4600+ words)
- 10 detailed steps dengan screenshots-ready
- Troubleshooting section
- Testing guide
- Deployment checklist
**Bonus:**
- ✅ New services (ConnectionMonitor, Constants)
- ✅ AppLifecycle observer
- ✅ 4 comprehensive documentation files
- ✅ Code formatted & error-free
- ✅ Production-ready architecture
---
## 🎯 Next Steps untuk Anda
1. **Read** `DEPLOYMENT_GUIDE.md` (mulai dari sini!)
2. **Deploy** Railway Worker (ikuti guide step-by-step)
3. **Test** semua fitur (Waktu, Sensor, History)
4. **Monitor** logs selama 24 jam pertama
5. **Enjoy** reliable 24/7 automation! 🎊
---
**Status:** ✅ PRODUCTION READY
**Version:** 2.0.0
**Date:** 10 Februari 2026
**🎊 Selamat! Aplikasi ApsGo Anda sekarang production-ready dengan automation 24/7!**
---
*"From local-only scheduling to cloud-powered 24/7 automation - ApsGo is now ready for the real world!"* 🚀

View File

@ -1,121 +0,0 @@
# 📱 Cara Install ApsGo di Handphone
## ✅ APK Sudah Selesai Di-build!
Lokasi APK: `f:\ApsGo\ApsGo\build\app\outputs\flutter-apk\app-release.apk`
---
## 🔧 Cara Install di Handphone
### **Metode 1: Transfer via USB Cable**
1. **Sambungkan HP ke PC** dengan kabel USB
2. **Copy file APK** dari:
```
f:\ApsGo\ApsGo\build\app\outputs\flutter-apk\app-release.apk
```
3. **Paste ke HP** (folder Download atau Internal Storage)
4. **Buka File Manager** di HP
5. **Tap file** `app-release.apk`
6. **Izinkan install** dari Unknown Sources (jika diminta)
7. **Install** → Selesai!
---
### **Metode 2: Transfer via WhatsApp/Telegram**
1. **Kirim file APK** ke diri sendiri via WhatsApp/Telegram:
- Buka WhatsApp Web/Desktop
- Kirim ke chat pribadi Anda
- File: `app-release.apk`
2. **Buka di HP** → Download file
3. **Tap file APK** → Install
---
### **Metode 3: Upload ke Google Drive**
1. **Upload APK** ke Google Drive dari PC
2. **Buka Google Drive** di HP
3. **Download APK**
4. **Install**
---
## ⚙️ Setting HP (Penting!)
Sebelum install, aktifkan **Install from Unknown Sources**:
### **Android 8.0+:**
1. Settings → Apps & notifications
2. Special app access
3. Install unknown apps
4. Pilih File Manager/Chrome
5. Allow from this source → ON
### **Android 7.0 dan lebih lama:**
1. Settings → Security
2. Unknown sources → ON
---
## 🔑 Login ke Aplikasi
Setelah install:
1. **Buka app ApsGo**
2. **Login** dengan:
- Email: (akun Firebase Anda)
- Password: (password akun)
3. **Atau Register** akun baru
---
## 📡 Koneksi ke ESP32
Pastikan:
- ✅ HP dan ESP32 sama-sama **terkoneksi internet**
- ✅ ESP32 sudah **tersambung ke WiFi**
- ✅ Firebase Realtime Database sudah **dikonfigurasi**
Data akan sync otomatis via Firebase!
---
## 🐛 Troubleshooting
**"App not installed":**
- Uninstall versi lama (jika ada)
- Coba install ulang
**"Unknown sources blocked":**
- Ikuti langkah Setting HP di atas
**"App keeps crashing":**
- Pastikan Google Services sudah aktif di HP
- Check koneksi internet
---
## 🔄 Update App
Jika ada update:
1. Build APK baru: `flutter build apk --release`
2. Transfer & install (akan replace versi lama)
---
## 📦 Informasi APK
- **Nama:** ApsGo (Project TA)
- **Package:** com.example.project_ta
- **Size:** ± 20-30 MB
- **Min Android:** 5.0 (Lollipop)
- **Firebase:** Integrated
---
**Selamat mencoba! 🎉**

View File

@ -1,284 +0,0 @@
# 📱 Panduan Sistem Penjad walan Baru - Flutter App
## ✨ Fitur Baru
Sistem penjadwalan ApsGo telah diupgrade dengan fitur:
**Multiple Jadwal** - Tidak lagi terbatas 2 jadwal saja
**Flexible Pot Selection** - Setiap jadwal bisa pilih pot mana yang aktif
**Per-Schedule Config** - Tiap jadwal punya durasi & pompa sendiri
**Enable/Disable** - Matikan jadwal tanpa hapus data
**Real-time Sync** - Langsung sync dengan Firebase & Railway Worker
## 📁 File Baru yang Dibuat
### Models
- `lib/models/jadwal_model.dart` - Data model untuk jadwal
### Services
- `lib/services/jadwal_service.dart` - CRUD operations untuk jadwal Firebase
### Screens
- `lib/screens/jadwal_management_page.dart` - List & manage all jadwal
- `lib/screens/jadwal_form_page.dart` - Add/edit jadwal form
### Config
- `firebase-structure-complete.json` - Struktur Firebase lengkap
## 🗄️ Struktur Firebase Baru
```json
{
"kontrol": {
"waktu": true,
"sensor": false,
"otomatis": true,
"jadwal_1": {
"aktif": true,
"waktu": "08:00",
"durasi": 60,
"pot_aktif": [1, 2, 3],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_2": {
"aktif": true,
"waktu": "09:00",
"durasi": 45,
"pot_aktif": [4, 5],
"pompa_air": true,
"pompa_pupuk": true
}
}
}
```
## 🚀 Cara Setup
### 1. Copy Struktur Firebase
1. Buka Firebase Console → Realtime Database
2. Edit `/kontrol` node
3. Tambahkan jadwal sesuai contoh di atas
4. Atau import dari `firebase-structure-complete.json`
### 2. Update Dependency (Jika Perlu)
Pastikan `pubspec.yaml` sudah punya:
```yaml
dependencies:
firebase_database: ^latest_version
shared_preferences: ^latest_version
```
### 3. Update Navigation
File `lib/screens/kontrol_page.dart` sudah diupdate untuk navigasi ke JadwalManagementPage.
## 📱 Cara Menggunakan di App
### Akses Jadwal Management
1. Buka app ApsGo
2. Tap tab **Kontrol**
3. Tap button **Waktu**
4. Akan muncul **Jadwal Management Page**
### Tambah Jadwal Baru
1. Di Jadwal Management Page, tap tombol **+ Tambah Jadwal**
2. Isi form:
- **Waktu**: Pilih jam berapa jadwal akan jalan
- **Durasi**: Berapa menit penyiraman (auto convert ke detik)
- **Pilih Pot**: Centang pot mana yang akan disiram
- **Pompa Air**: Toggle on/off
- **Pompa Pupuk**: Toggle on/off
- **Status Jadwal**: Aktif or nonaktif
3. Tap **TAMBAH JADWAL**
### Edit Jadwal
1. Di list jadwal, tap button **Edit**
2. Ubah settingan yang diinginkan
3. Tap **SIMPAN PERUBAHAN**
### Aktifkan/Nonaktifkan Jadwal
Gunakan **Switch** di kanan atas setiap jadwal card untuk toggle on/off cepat.
### Duplikat Jadwal
1. Tap button **Duplikat** di jadwal yang ingin dicopy
2. Jadwal baru otomatis dibuat dengan ID baru (status: nonaktif)
3. Edit jadwal baru sesuai kebutuhan
### Hapus Jadwal
1. Tap icon **🗑️** (trash) di jadwal yang ingin dihapus
2. Konfirmasi penghapusan
## 🎨 UI Features
### Jadwal Management Page
- **Mode Waktu Toggle**: Enable/disable semua jadwal sekaligus
- **Jadwal Cards**: Menampilkan semua jadwal dengan info lengkap
- **Color Coding**: Hijau = aktif, Abu-abu = nonaktif
- **Real-time Updates**: Pull to refresh untuk sync terbaru
### Jadwal Form Page
- **Time Picker**: Native Android/iOS time picker
- **Duration Input**: Input menit, auto convert ke detik
- **Pot Selection**: Visual checkboxes untuk 5 pot
- **Quick Actions**: "Pilih Semua" & "Bersihkan"
- **Validation**: Auto validasi sebelum save
## ⚠️ Catatan Penting
### Kompatibilitas dengan Worker
Worker Railway sudah updated untuk support format baru. Kedua sistem (lama & baru) masih compatible:
**Format Lama** (masih jalan):
```json
{
"waktu_1": "08:00",
"durasi_1": 60
}
```
**Format Baru** (recommended):
```json
{
"jadwal_1": {
"aktif": true,
"waktu": "08:00",
"durasi": 60,
"pot_aktif": [1, 2, 3],
"pompa_air": true,
"pompa_pupuk": false
}
}
```
### Migration Path
Tidak perlu migrate semua sekaligus. Sistem bisa pakai campuran format lama & baru.
**Recommended migration:**
1. Biarkan format lama tetap jalan
2. Tambah jadwal baru dengan format baru
3. Test jadwal baru beberapa hari
4. Hapus format lama setelah yakin
## 🔧 Troubleshooting
### Jadwal tidak muncul di app
**Check:**
1. Firebase connected? (Check Firebase console)
2. Struktur Firebase benar?
3. Node path: `/kontrol/jadwal_X`
### Jadwal tidak trigger di worker
**Check:**
1. Worker running di Railway?
2. `kontrol.waktu` = `true`?
3. `jadwal_X.aktif` = `true`?
4. Format waktu "HH:MM" (2 digit)?
5. Check Railway logs
### Pot tidak menyala
**Check:**
1. `pot_aktif` array benar? `[1, 2, 3]`
2. Pot number 1-5 (bukan 0-4)
3. Check aktuator di Firebase
4. Hardware connection OK?
## 📊 Testing Checklist
Sebelum production, test:
- [ ] Add jadwal baru
- [ ] Edit jadwal existing
- [ ] Toggle jadwal aktif/nonaktif
- [ ] Duplikat jadwal
- [ ] Hapus jadwal
- [ ] Toggle mode waktu on/off
- [ ] Test jadwal trigger di waktu yang ditentukan
- [ ] Test multiple pot selection
- [ ] Test pompa air on/off per jadwal
- [ ] Test pompa pupuk on/off per jadwal
- [ ] Test pull to refresh
- [ ] Test dengan 5+ jadwal sekaligus
## 🎯 Contoh Use Case
### Use Case 1: Rumah simple
**Kebutuhan**: Siram semua pot 2x sehari (pagi & sore)
**Setup**:
```
Jadwal 1: 08:00 → Pot [1,2,3,4,5] → 60s
Jadwal 2: 16:00 → Pot [1,2,3,4,5] → 60s
```
### Use Case 2: Per-pot berbeda
**Kebutuhan**:
- Pot 1&2: 3x sehari (pagi, siang, sore)
- Pot 3&4: 2x sehari (pagi, sore)
- Pot 5: 1x sehari (pagi)
**Setup**:
```
Jadwal 1: 07:00 → Pot [1,2] → 60s
Jadwal 2: 08:00 → Pot [3,4,5] → 60s
Jadwal 3: 12:00 → Pot [1,2] → 45s
Jadwal 4: 16:00 → Pot [3,4] → 50s
Jadwal 5: 17:00 → Pot [1,2] → 40s
```
### Use Case 3: Different durations
**Kebutuhan**: Setiap pot punya durasi berbeda
**Setup**:
```
Jadwal 1: 08:00 → Pot [1] → 30s (Fast)
Jadwal 2: 08:05 → Pot [2] → 60s (Medium)
Jadwal 3: 08:10 → Pot [3] → 90s (Long)
Jadwal 4: 08:15 → Pot [4] → 45s
Jadwal 5: 08:20 → Pot [5] → 75s
```
## 📝 Best Practices
1. **Naming Convention**: Gunakan `jadwal_1`, `jadwal_2`, dst (agar auto-detect worker)
2. **Time Format**: Selalu 2 digit "HH:MM" (misal: "08:00" bukan "8:0")
3. **Duration**: Input dalam menit di app, akan auto convert ke detik
4. **pot_aktif**: Gunakan array number [1, 2, 3], bukan string
5. **Testing**: Test jadwal baru dengan status nonaktif dulu sebelum aktifkan
## 🔗 Related Files
- Worker: `railway-worker/worker.js` (sudah updated)
- Firebase Guide: `railway-worker/FLEXIBLE_SCHEDULE_GUIDE.md`
- Example Firebase: `firebase-structure-complete.json`
---
## 💡 Tips
- Gunakan **Duplikat** untuk cepat bikin jadwal mirip
- **Pull to refresh** untuk sync perubahan dari Firebase/Worker
- **Mode Waktu toggle** untuk disable semua jadwal sekaligus
- Jadwal dengan **pot kosong** akan di-skip otomatis
Selamat menggunakan sistem penjadwalan baru! 🚀🌱

View File

@ -1,185 +0,0 @@
# 🚀 Quick Setup Firebase Jadwal ke kontrol_1
## ✅ Yang Sudah Diupdate
1. ✅ **Worker.js** - Sekarang pakai path `/kontrol_1`
2. ✅ **jadwal_service.dart** - Flutter app pakai path `/kontrol_1`
3. ✅ **Setup script** - Script untuk push data otomatis ke Firebase
## 🔥 Cara Setup Data ke Firebase
### Opsi 1: Via Script (Recommended - Otomatis)
Jalankan script untuk langsung push data ke Firebase:
```bash
cd f:\ApsGo\ApsGo\railway-worker
node setup-firebase-jadwal.js
```
Script akan:
- ✅ Connect ke Firebase kamu
- ✅ Push data jadwal ke `/kontrol_1`
- ✅ Setup 5 jadwal (3 aktif, 2 nonaktif)
- ✅ Log summary hasil setup
**Output yang diharapkan:**
```
🚀 Starting Firebase setup...
📍 Path: /kontrol_1
✅ Firebase setup completed successfully!
📊 Summary:
- Path: /kontrol_1
- Mode Waktu: ENABLED
- Total Jadwal: 5
- Jadwal Aktif: 3
📝 Jadwal yang di-setup:
✅ jadwal_1: 08:00 → Pot [1, 2, 3] (60s)
✅ jadwal_2: 09:00 → Pot [4, 5] (45s)
✅ jadwal_3: 16:00 → Pot [1, 2, 3, 4, 5] (30s)
❌ jadwal_4: 12:00 → Pot [2, 4] (40s)
❌ jadwal_5: 18:00 → Pot [1, 5] (50s)
🎉 Done! Data sudah tersimpan di Firebase.
```
### Opsi 2: Manual via Firebase Console
1. Buka [Firebase Console](https://console.firebase.google.com)
2. Pilih project ApsGo
3. Klik **Realtime Database**
4. Klik `+` untuk add new node
5. Name: `kontrol_1`
6. Copy-paste JSON dari `firebase-structure-complete.json`
## 📱 Test Flutter App
Setelah data di-setup, test Flutter app:
```bash
cd f:\ApsGo\ApsGo
flutter pub get
flutter run
```
**Test checklist:**
- [ ] Buka tab **Kontrol**
- [ ] Tap button **Waktu**
- [ ] Lihat list jadwal (harus ada 5 jadwal)
- [ ] Toggle mode waktu on/off
- [ ] Tambah jadwal baru
- [ ] Edit jadwal existing
- [ ] Delete jadwal
## 🔄 Deploy ke Railway
Setelah verify lokal OK, push ke Railway:
```bash
cd railway-worker
git add .
git commit -m "feat: Update to use kontrol_1 path + flexible jadwal system"
git push origin main
```
Railway akan auto-deploy dalam 2-3 menit.
## 📊 Verify di Railway Logs
Setelah deploy, check logs Railway:
```
🚀 Starting ApsGo Railway Worker...
📍 Kontrol Path: /kontrol_1
✅ Firebase /kontrol_1 readable - waktu mode: ENABLED
📋 Total Jadwal: 3
✅ jadwal_1: 08:00 → Pot [1, 2, 3]
✅ jadwal_2: 09:00 → Pot [4, 5]
✅ jadwal_3: 16:00 → Pot [1, 2, 3, 4, 5]
```
## ⚠️ Troubleshooting
### Script error: "Firebase initialization failed"
**Check:**
1. File `.env` ada di folder `railway-worker`?
2. Environment variables lengkap?
```
FIREBASE_PROJECT_ID=...
FIREBASE_CLIENT_EMAIL=...
FIREBASE_PRIVATE_KEY=...
FIREBASE_DATABASE_URL=...
```
### Flutter app: "No jadwal found"
**Check:**
1. Path di `jadwal_service.dart` = `kontrol_1`? ✅ (sudah diupdate)
2. Data sudah di-push ke Firebase?
3. Firebase rules allow read?
### Worker tidak detect jadwal
**Check:**
1. Worker sudah redeploy dengan code terbaru?
2. Path di worker = `kontrol_1`? ✅ (sudah diupdate)
3. Railway logs show path `/kontrol_1`?
## 🔧 Ubah Path Kembali ke /kontrol
Jika mau pakai path `/kontrol` (bukan `/kontrol_1`):
**Worker.js:**
```javascript
const FIREBASE_PATHS = {
kontrol: 'kontrol', // Ubah dari 'kontrol_1' ke 'kontrol'
```
**jadwal_service.dart:**
```dart
static const String kontrolPath = 'kontrol'; // Ubah dari 'kontrol_1'
```
**Setup script:**
```javascript
await db.ref('kontrol').set(kontrolData); // Ubah dari 'kontrol_1'
```
## 📋 Summary Changes
**Files modified:**
- ✅ `railway-worker/worker.js` - Added FIREBASE_PATHS constant, updated all refs
- ✅ `lib/services/jadwal_service.dart` - Updated path to kontrol_1
- ✅ `railway-worker/setup-firebase-jadwal.js` - New script untuk setup
**Data structure:**
```
Firebase Root
├── aktuator/
├── data/
├── history/
├── kontrol/ ← Your existing data (unchanged)
└── kontrol_1/ ← New data with flexible jadwal
├── waktu: true
├── sensor: false
├── jadwal_1/
├── jadwal_2/
└── jadwal_3/
```
## 🎯 Next Steps
1. ✅ Run setup script: `node setup-firebase-jadwal.js`
2. ✅ Verify data di Firebase Console
3. ✅ Test Flutter app locally
4. ✅ Commit & push to Railway
5. ✅ Monitor Railway logs
6. ✅ Test jadwal trigger di waktu yang ditentukan
---
**Ready to go!** 🚀🌱
Jika ada error atau pertanyaan, check logs atau tanya!

View File

@ -1,111 +0,0 @@
# 🚀 Quick Start - Railway Worker Setup
## ✅ Apa yang Sudah Dibuat
### 1. Railway Worker (Node.js)
```
railway-worker/
├── worker.js # Main worker code
├── package.json # Dependencies
├── railway.json # Railway config
├── .env.example # Environment template
└── README.md # Worker documentation
```
**Features:**
- ✅ Waktu Mode (time-based scheduling)
- ✅ Sensor Mode (threshold automation)
- ✅ Auto history logging (10 min)
- ✅ Redis queue (prevent race conditions)
- ✅ Graceful shutdown & error handling
### 2. Flutter Bug Fixes
- ✅ Memory leak fixed (StreamSubscription disposal)
- ✅ AppLifecycle observer (stop services saat background)
- ✅ Connection monitoring service
- ✅ Constants untuk configuration
- ✅ Improved error handling
### 3. Documentation
- ✅ `DEPLOYMENT_GUIDE.md` - Langkah deploy ke Railway
- ✅ `BUGS_AND_FIXES.md` - Bug report & fixes
- ✅ `railway-worker/README.md` - Worker docs
---
## 📋 Langkah Deployment (Ringkas)
### Step 1: Firebase Setup
1. Download Service Account Key dari Firebase Console
2. Simpan: `project_id`, `client_email`, `private_key`
### Step 2: Railway Setup
1. Login ke [railway.app](https://railway.app)
2. Create new project → Deploy from GitHub
3. Add Redis database
4. Set root directory: `railway-worker`
### Step 3: Environment Variables
Di Railway, tambahkan:
- `FIREBASE_PROJECT_ID`
- `FIREBASE_CLIENT_EMAIL`
- `FIREBASE_PRIVATE_KEY`
- `FIREBASE_DATABASE_URL`
### Step 4: Deploy!
Railway auto-deploy setelah variables di-set.
### Step 5: Test
- Check logs untuk "Worker is running"
- Test waktu mode dari Flutter app
- Test sensor mode
- Monitor logs
**📖 Detail lengkap:** Lihat `DEPLOYMENT_GUIDE.md`
---
## 🎯 Kesimpulan
### Masalah yang Diselesaikan
1. ✅ **Scheduling hanya jalan saat app buka** → Sekarang 24/7 dengan Railway
2. ✅ **Memory leaks** → Fixed dengan proper disposal
3. ✅ **Race conditions** → Fixed dengan BullMQ queue
4. ✅ **No connection check** → Added monitoring service
### Arsitektur Baru
```
Flutter App ↔ Firebase RTDB ↔ Railway Worker (24/7) ↔ ESP32
Redis Queue
```
### Biaya
- Railway Free Tier: $5/month credit (cukup untuk IoT project)
- Estimated usage: $3-5/month
---
## 🆘 Troubleshooting
**Worker tidak start:**
- Check `FIREBASE_PRIVATE_KEY` format (harus ada `\n`)
- Verify Redis service aktif
**Jadwal tidak trigger:**
- Check timezone (worker use UTC, convert dari local)
- Verify `/kontrol/waktu` = true
**Detail:** Lihat DEPLOYMENT_GUIDE.md section Troubleshooting
---
## 📞 Support
- Railway Discord: discord.gg/railway
- Firebase Support: firebase.google.com/support
- Project Issues: GitHub repository
---
**Selamat! Sistem Anda sekarang production-ready dengan automation 24/7! 🎉**

View File

@ -1,226 +0,0 @@
# 🚨 URGENT: Railway Worker Setup - Step by Step
## ❌ Masalah: Tidak Ada Log di Railway
Berdasarkan screenshot Firebase Anda, **data kontrol sudah BENAR** ✅:
```json
{
"waktu": true,
"waktu_1": "06:41",
"waktu_2": "16:38",
"durasi_1": 600,
"durasi_2": 104
}
```
**Root Cause:** Railway Worker belum running atau tidak bisa akses Firebase.
---
## ✅ SOLUSI LENGKAP
### 🎯 Opsi 1: Test Lokal Dulu (RECOMMENDED)
Sebelum deploy ke Railway, test koneksi Firebase di komputer Anda:
#### Step 1: Download Firebase Service Account Key
1. Buka [Firebase Console](https://console.firebase.google.com)
2. Pilih project **Project TA** (project-ta-951b4)
3. Klik ⚙️ **Project Settings** (pojok kiri bawah)
4. Tab **Service Accounts**
5. Klik **Generate New Private Key**
6. Download file JSON (jangan share ke siapapun!)
#### Step 2: Setup Environment Variables Lokal
1. Buka file JSON yang di-download
2. Copy 3 nilai ini:
```json
{
"project_id": "project-ta-951b4",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com"
}
```
3. Buat file `.env` di folder `railway-worker/`:
```bash
cd f:\ApsGo\ApsGo\railway-worker
copy .env.template .env
notepad .env
```
4. Isi dengan credentials dari JSON:
```env
FIREBASE_PROJECT_ID=project-ta-951b4
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----\n"
FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com
```
**PENTING untuk FIREBASE_PRIVATE_KEY:**
- Harus ada quotes: `"..."`
- Harus ada `\n` di awal dan akhir
- Jangan ubah format dari JSON
#### Step 3: Test Koneksi Firebase
```powershell
cd f:\ApsGo\ApsGo\railway-worker
npm run test:firebase
```
**Expected output:**
```
✅ All required environment variables are set
✅ Firebase Admin SDK initialized
✅ Successfully read /kontrol data
✅ Waktu mode is ENABLED
✅ Jadwal 1: 06:41
✅ Jadwal 2: 16:38
```
**Jika berhasil:**
- Credentials Anda benar ✅
- Firebase accessible ✅
- Siap deploy ke Railway ✅
**Jika error:**
- Check private key format (harus ada quotes dan \n)
- Check client email benar
- Check Firebase Database rules allow admin access
#### Step 4: Test Worker Lokal (Optional)
Jika koneksi OK, test worker secara lokal:
```powershell
# Install Redis lokal (optional, skip jika tidak punya)
# Atau comment out Redis parts di worker.js untuk test
# Run worker
npm run dev
```
Tunggu sampai waktu schedule (06:41 atau 16:38), worker akan trigger otomatis.
---
### 🎯 Opsi 2: Deploy Langsung ke Railway
Jika test lokal berhasil, deploy ke Railway:
#### Step 1: Commit Changes
```powershell
cd f:\ApsGo\ApsGo
git add .
git commit -m "Update Railway Worker configuration"
git push origin main
```
#### Step 2: Setup Railway Environment Variables
1. Login ke [Railway Dashboard](https://railway.app)
2. Pilih project ApsGo
3. Klik service **worker**
4. Tab **Variables**
5. Tambahkan 4 variables (gunakan nilai yang sama dari .env lokal):
| Variable | Value |
|----------|-------|
| `FIREBASE_PROJECT_ID` | `project-ta-951b4` |
| `FIREBASE_CLIENT_EMAIL` | `firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com` |
| `FIREBASE_PRIVATE_KEY` | `"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"` |
| `FIREBASE_DATABASE_URL` | `https://project-ta-951b4-default-rtdb.firebaseio.com` |
**CRITICAL:** Copy-paste EXACT values from your working local .env file!
#### Step 3: Redeploy
Railway will auto-redeploy after adding variables, or manually:
1. Tab **Deployments**
2. Klik **...** (three dots)
3. **Redeploy**
#### Step 4: Check Logs
Tab **Logs**, expected output:
```
🚀 Starting ApsGo Railway Worker...
✅ All required environment variables are set
✅ Firebase Admin initialized
✅ Redis connected
✨ ApsGo Railway Worker is running!
⏰ Checking scheduled watering...
```
---
## 🔍 Quick Troubleshooting
### Error: "Missing environment variables"
→ .env file tidak ada atau belum diisi
**Fix:** Ikuti Step 2 di atas
### Error: "Firebase initialization failed"
→ Private key format salah
**Fix:** Copy EXACT value dari JSON, dengan quotes dan \n
### Error: "PERMISSION_DENIED"
→ Firebase rules tidak allow service account
**Fix:** Check Firebase Realtime Database Rules
### No Error, tapi No Logs
→ Worker belum deploy/running
**Fix:** Check Railway Deployments tab
---
## 📊 Verifikasi Data Firebase (From Your Screenshot)
✅ Your Firebase data is **CORRECT**:
```
kontrol/
├─ waktu: true ✅ Mode enabled
├─ waktu_1: "06:41" ✅ Schedule 1
├─ waktu_2: "16:38" ✅ Schedule 2
├─ durasi_1: 600 ✅ 10 minutes
├─ durasi_2: 104 ✅ 1.7 minutes
├─ batas_atas: 80 ✅ Sensor threshold
├─ batas_bawah: 50 ✅
├─ otomatis: false Sensor mode disabled
└─ mode_sensor: "smart" ✅
```
**Flutter app sudah benar!** Masalahnya di Railway Worker yang belum running.
---
## 🎯 Next Steps
**MULAI DARI SINI:**
1. ✅ Test koneksi Firebase lokal:
```powershell
cd f:\ApsGo\ApsGo\railway-worker
npm run test:firebase
```
2. ✅ Jika berhasil, deploy ke Railway (ikuti Opsi 2)
3. ✅ Monitor Railway logs
4. ✅ Test dengan set jadwal 2-3 menit dari sekarang
---
## 📞 Need Help?
Jika masih stuck:
1. Screenshot output dari `npm run test:firebase`
2. Screenshot Railway logs (jika sudah deploy)
3. Screenshot Firebase Service Account settings
Mari kita debug bersama!

View File

@ -1,148 +0,0 @@
# ❌ Railway Worker Troubleshooting Guide
## Masalah: Jadwal tidak berjalan, tidak ada log di Railway
### ✅ Langkah Diagnosis:
#### 1. Check Railway Worker Status
Di Railway Dashboard:
- Masuk ke project ApsGo
- Klik service **worker**
- Tab **Deployments** → Lihat status deployment terakhir
- Tab **Logs** → Periksa apakah worker running
**Expected logs:**
```
🚀 Starting ApsGo Railway Worker...
📡 Firebase Project: project-ta-951b4
📦 Redis: xxxx:6379
⏰ Timezone: Asia/Jakarta (Current: 11/02/2026 14:30:00)
✅ All required environment variables are set
✅ Firebase Admin initialized
✅ Redis connected
✨ ApsGo Railway Worker is running!
```
#### 2. Test Environment Variables
Di Railway Console, jalankan debug script:
```bash
# Di Railway service console
npm run debug
```
Atau manual check:
- Go to **Variables** tab
- Pastikan ada 4 variables ini:
- `FIREBASE_PROJECT_ID` = project-ta-951b4
- `FIREBASE_CLIENT_EMAIL` = firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com
- `FIREBASE_PRIVATE_KEY` = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
- `FIREBASE_DATABASE_URL` = https://project-ta-951b4-default-rtdb.firebaseio.com
#### 3. Test Firebase Configuration
Check apakah Flutter app berhasil menyimpan jadwal:
- Buka Firebase Console → Realtime Database
- Check node `/kontrol`
- Harus ada data seperti:
```json
{
"waktu": true,
"waktu_1": "08:00",
"waktu_2": "16:00",
"durasi_1": 600,
"durasi_2": 600
}
```
#### 4. Time Sync Check
- Railway Worker menggunakan timezone Asia/Jakarta
- Check apakah jadwal yang di-set di Flutter sesuai dengan waktu sekarang
- Test dengan set jadwal 1-2 menit dari waktu sekarang
---
## 🛠️ Solusi untuk Masalah Umum
### Problem: "Firebase initialization failed"
**Cause:** Environment variables salah/kurang
**Solution:**
1. Re-download Service Account Key dari Firebase Console
2. Extract 4 values yang dibutuhkan
3. Set ulang variables di Railway
4. **PENTING:** `FIREBASE_PRIVATE_KEY` harus dalam quotes dan dengan `\n` escaped
### Problem: "Redis connection error"
**Cause:** Redis service tidak active
**Solution:**
1. Railway Dashboard → Add Redis Database
2. Link ke worker service
3. Restart worker
### Problem: "Worker tidak trigger jadwal"
**Cause:** Timezone mismatch atau konfigurasi salah
**Solution:**
1. Check `/kontrol` di Firebase ada data
2. Check `waktu: true`
3. Check jadwal format "HH:MM" (e.g., "08:00")
4. Test dengan jadwal dekat waktu sekarang
### Problem: "No logs di Railway"
**Cause:** Worker crash saat startup
**Solution:**
1. Check **Deployments** tab - ada error saat build?
2. Check **Logs** tab - ada error message?
3. Run debug script: `npm run debug`
---
## 🔍 Debug Commands
### Local Debug (di komputer Anda)
```bash
cd railway-worker
npm install
node debug.js
```
### Railway Debug (di Railway Console)
```bash
npm run debug
```
### Manual Test (di Railway Console)
```bash
# Check environment
env | grep FIREBASE
# Test Firebase connection
node -e "console.log('DB URL:', process.env.FIREBASE_DATABASE_URL)"
# Test current time
node -e "console.log('Time:', new Date().toLocaleString('id-ID', {timeZone: 'Asia/Jakarta'}))"
```
---
## ⚡ Quick Fix Checklist
- [ ] Railway worker service running (green status)
- [ ] All 4 Firebase environment variables set correctly
- [ ] Redis database service added and linked
- [ ] Firebase Realtime Database rules allow admin access
- [ ] Flutter app successfully saved schedule (`/kontrol` node exists)
- [ ] Timezone configured as Asia/Jakarta
- [ ] Test schedule set within 1-2 minutes from current time
---
## 📞 Still Having Issues?
1. Export Railway logs: **Logs** tab → Copy all logs
2. Export Firebase `/kontrol` data: Firebase Console → Export JSON
3. Screenshot Flutter schedule configuration
4. Run `npm run debug` dan copy output
With this information, the issue can be diagnosed precisely.

View File

@ -1,280 +0,0 @@
# 🔥 Setup Firebase untuk Sistem Jadwal Baru
## 📋 Copy Struktur Ini ke Firebase
Buka Firebase Console → Realtime Database → Edit `/kontrol` node:
```json
{
"batas_atas": 32,
"batas_bawah": 11,
"durasi_sensor": 600,
"mode_sensor": "smart",
"otomatis": true,
"sensor": false,
"waktu": true,
"jadwal_1": {
"aktif": true,
"waktu": "08:00",
"durasi": 60,
"pot_aktif": [1, 2, 3],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_2": {
"aktif": true,
"waktu": "09:00",
"durasi": 45,
"pot_aktif": [4, 5],
"pompa_air": true,
"pompa_pupuk": true
},
"jadwal_3": {
"aktif": true,
"waktu": "16:00",
"durasi": 30,
"pot_aktif": [1, 2, 3, 4, 5],
"pompa_air": true,
"pompa_pupuk": true
}
}
```
## 🎯 Contoh Sesuai Request Kamu
**Kebutuhan:**
- Jam 8 pagi: Pot 1, 2, 3 aktif
- Jam 9 pagi: Pot 4, 5 aktif
- Jam 4 sore: Pot 1, 2, 3, 4, 5 aktif
**Setup Firebase:**
```json
{
"waktu": true,
"jadwal_1": {
"aktif": true,
"waktu": "08:00",
"durasi": 60,
"pot_aktif": [1, 2, 3],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_2": {
"aktif": true,
"waktu": "09:00",
"durasi": 60,
"pot_aktif": [4, 5],
"pompa_air": true,
"pompa_pupuk": true
},
"jadwal_3": {
"aktif": true,
"waktu": "16:00",
"durasi": 60,
"pot_aktif": [1, 2, 3, 4, 5],
"pompa_air": true,
"pompa_pupuk": true
}
}
```
## Cara Tambah Jadwal Baru di Firebase
### Via Firebase Console (Manual)
1. Buka Firebase Console
2. Go to Realtime Database
3. Navigate ke `/kontrol`
4. Click `+` untuk add child
5. Name: `jadwal_4`
6. Add children:
```
jadwal_4:
├─ aktif: true
├─ waktu: "12:00"
├─ durasi: 45
├─ pot_aktif: [2, 4]
├─ pompa_air: true
└─ pompa_pupuk: false
```
### Via Flutter App (Recommended)
1. Buka ApsGo App
2. Tap **Kontrol** tab
3. Tap **Waktu** button
4. Tap tombol **+ Tambah Jadwal**
5. Isi form sesuai kebutuhan
6. Tap **TAMBAH JADWAL**
**Keuntungan via App:**
- ✅ Lebih mudah & cepat
- ✅ Auto validasi
- ✅ Auto generate ID
- ✅ Visual pot selection
- ✅ Time picker
## 📱 Cara Test di App
### 1. Run Flutter App
```bash
cd f:\ApsGo\ApsGo
flutter run
```
### 2. Navigate ke Jadwal
1. Login ke app
2. Tap tab **Kontrol** di bottom navigation
3. Tap button **Waktu**
4. Akan muncul **Jadwal Management Page**
### 3. Test Features
- ✅ Lihat semua jadwal
- ✅ Toggle mode waktu on/off
- ✅ Tambah jadwal baru
- ✅ Edit jadwal
- ✅ Toggle jadwal aktif/nonaktif
- ✅ Duplikat jadwal
- ✅ Hapus jadwal
- ✅ Pull to refresh
## 🔄 Migration dari Format Lama
### Format Lama Kamu (di screenshot):
```json
{
"waktu": true,
"waktu_1": "10:02",
"durasi_1": 60,
"waktu_2": "16:00",
"durasi_2": 600
}
```
### Konversi ke Format Baru:
```json
{
"waktu": true,
"jadwal_1": {
"aktif": true,
"waktu": "10:02",
"durasi": 60,
"pot_aktif": [1, 2, 3, 4, 5],
"pompa_air": true,
"pompa_pupuk": true
},
"jadwal_2": {
"aktif": true,
"waktu": "16:00",
"durasi": 600,
"pot_aktif": [1, 2, 3, 4, 5],
"pompa_air": true,
"pompa_pupuk": true
}
}
```
**Note:** Format lama masih jalan di worker (backward compatible), jadi tidak perlu buru-buru migrate.
## ⚙️ Field Explanations
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `aktif` | boolean | No | `true` | Enable/disable jadwal |
| `waktu` | string | Yes | - | Format "HH:MM" (08:00, 16:00, dll) |
| `durasi` | number | No | `60` | Durasi dalam DETIK |
| `pot_aktif` | array | Yes | - | Array pot: [1, 2, 3, 4, 5] |
| `pompa_air` | boolean | No | `true` | Nyalakan pompa air |
| `pompa_pupuk` | boolean | No | `false` | Nyalakan pompa pupuk |
## ✅ Validation Rules
Firebase akan accept jadwal jika:
1. ✅ `waktu` format "HH:MM" dengan 2 digit
2. ✅ `durasi` > 0
3. ✅ `pot_aktif` tidak kosong
4. ✅ `pot_aktif` berisi angka 1-5
Worker akan skip jadwal jika:
- ❌ `aktif` = false
- ❌ `pot_aktif` kosong
- ❌ `waktu` format salah
## 🚀 Deploy Changes
### 1. Update Flutter App
```bash
cd f:\ApsGo\ApsGo
flutter pub get
flutter build apk --release
```
### 2. Update Railway Worker (Sudah Done!)
Worker sudah updated dan di-push ke GitHub.
### 3. Setup Firebase
Copy struktur JSON di atas ke Firebase Console.
### 4. Test Everything
1. Test add jadwal via app
2. Test edit jadwal
3. Test toggle aktif/nonaktif
4. Wait sampai waktu jadwal
5. Check logs Railway untuk verifikasi
## 📊 Expected Logs Railway
Setelah setup, di Railway logs kamu akan lihat:
```
⏱️ CHECK #15: 08:00:05 | Mode: ✅
📋 Total Jadwal: 3
✅ jadwal_1: 08:00 → Pot [1, 2, 3] 🔔 MATCH!
✅ jadwal_2: 09:00 → Pot [4, 5]
✅ jadwal_3: 16:00 → Pot [1, 2, 3, 4, 5]
🕐 JADWAL_1 TRIGGERED: 08:00
🎯 Pot aktif: [1, 2, 3]
⏱️ Durasi: 60s
💧 Pompa Air: ON
🌿 Pompa Pupuk: OFF
✅ Successfully added to queue
```
## 🎉 You're All Set!
Sistem penjadwalan baru siap digunakan! Fitur:
✅ Multiple jadwal (unlimited)
✅ Flexible pot selection
✅ Per-schedule configuration
✅ Easy add/edit/delete via app
✅ Real-time sync
✅ Auto validation
**Next Steps:**
1. Setup Firebase dengan struktur di atas
2. Run Flutter app
3. Test tambah/edit jadwal
4. Monitor Railway logs
5. Enjoy! 🚀🌱

View File

@ -1,308 +0,0 @@
# 🚨 SOLUSI LENGKAP: Penjadwalan Tidak Masuk Railway
## 📋 HASIL ANALISIS
### ✅ Yang SUDAH BENAR:
1. **Flutter Code** - Data penjadwalan ditulis dengan benar ke Firebase
2. **Worker Code** - Logic membaca dan execute sudah benar
3. **Format Data** - Compatible antara Flutter dan Worker
4. **Git Repository** - railway-worker sudah di-commit dan push
### ❌ KEMUNGKINAN MASALAH:
1. **Railway Root Directory belum di-set**
2. **Environment Variables belum lengkap/salah**
3. **Redis service belum di-add ke Railway**
4. **Data penjadwalan belum di-set dari Flutter**
---
## 🔧 LANGKAH PERBAIKAN (STEP-BY-STEP)
### **STEP 1: Verifikasi Data Di Firebase Console**
1. Buka [Firebase Console](https://console.firebase.google.com)
2. Pilih project: **project-ta-951b4**
3. Klik **Realtime Database** di sidebar
4. Cek path `/kontrol/`
**Yang HARUS ADA:**
```json
{
"kontrol": {
"waktu": true, ← MUST BE TRUE!
"waktu_1": "07:30", ← Format HH:mm string
"waktu_2": "17:00", ← Format HH:mm string
"durasi_1": 60, ← Integer (seconds)
"durasi_2": 60, ← Integer (seconds)
"otomatis": false,
"batas_bawah": 40,
"batas_atas": 100
}
}
```
**Jika data TIDAK ADA atau `waktu` = false:**
- Buka Flutter app
- Masuk ke **Kontrol Waktu**
- Set jadwal
- **Toggle "Aktif" menjadi ON**
- Klik **Simpan**
---
### **STEP 2: Test Firebase Connection Dari Local**
Di folder railway-worker, buat file `.env`:
```bash
cd f:\ApsGo\ApsGo\railway-worker
```
Create `.env` file (copy dari .env.example):
```env
FIREBASE_PROJECT_ID=project-ta-951b4
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYourKeyHere\n-----END PRIVATE KEY-----\n"
FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com
```
**Cara dapat Firebase credentials:**
1. Firebase Console → Project Settings
2. Service Accounts tab
3. Klik "Generate New Private Key"
4. Download JSON file
5. Copy value dari JSON:
- `project_id` → FIREBASE_PROJECT_ID
- `client_email` → FIREBASE_CLIENT_EMAIL
- `private_key` → FIREBASE_PRIVATE_KEY (ganti \n dengan \\n)
- Database URL bisa dari Realtime Database page
**Test connection:**
```powershell
cd railway-worker
npm install
node test-firebase-connection.js
```
**Expected output:**
```
✅ Firebase initialized
✅ DATA FOUND! Structure:
✅ DATA STRUCTURE VALID
✅ WAKTU MODE ENABLED
```
**Jika error:**
- Check credentials (terutama PRIVATE_KEY format)
- Pastikan Database URL benar
- Pastikan Firebase Rules mengizinkan read/write
---
### **STEP 3: Setup Railway - Root Directory**
1. **Login ke Railway:** https://railway.app
2. **Select Project:** mellow-cat
3. **Select Service:** myreppril
4. **Klik tab "Settings"**
5. **Scroll ke section "Build"**
6. **Cari field "Root Directory"**
7. **Isi dengan:** `railway-worker`
8. **Save** (auto-save)
**PENTING:** Tanpa ini, Railway akan build dari root folder dan gagal!
---
### **STEP 4: Add Redis Database**
1. Di Railway project dashboard
2. Klik tombol **"+ New"**
3. Pilih **"Database"**
4. Pilih **"Add Redis"**
5. Tunggu provisioning selesai
Railway akan auto-inject environment variables:
- `REDIS_HOST`
- `REDIS_PORT`
- `REDIS_PASSWORD`
---
### **STEP 5: Set Environment Variables**
Di Railway → Service myreppril → Tab **"Variables"**
**ADD THESE:**
| Variable Name | Value |
|--------------|-------|
| `FIREBASE_PROJECT_ID` | `project-ta-951b4` |
| `FIREBASE_CLIENT_EMAIL` | `firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com` |
| `FIREBASE_PRIVATE_KEY` | `-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----` |
| `FIREBASE_DATABASE_URL` | `https://project-ta-951b4-default-rtdb.firebaseio.com` |
**⚠️ PENTING untuk PRIVATE_KEY:**
- Harus dalam format string dengan `\n` (backslash-n), BUKAN newline sebenarnya
- Contoh: `-----BEGIN PRIVATE KEY-----\nMIIEvQIBADA...\n-----END PRIVATE KEY-----\n`
- Copy dari JSON file service account, tapi ganti newline dengan `\n`
**Cara mudah:**
```javascript
// Di PowerShell atau Node.js console
const fs = require('fs');
const key = JSON.parse(fs.readFileSync('service-account.json')).private_key;
console.log(key); // Ini sudah format dengan \n
```
---
### **STEP 6: Redeploy Railway**
1. Setelah set Root Directory & Variables
2. Klik tab **"Deployments"**
3. Klik **"Redeploy"** atau biarkan auto-deploy
4. Tunggu build selesai (2-3 menit)
---
### **STEP 7: Monitor Logs**
1. Klik tab **"Logs"** (di navigation bar atas)
2. Enable "Follow logs" (toggle di kanan atas)
**Expected logs saat startup:**
```
🚀 Starting ApsGo Railway Worker...
📡 Firebase Project: project-ta-951b4
📦 Redis: redis.railway.internal:6379
✅ Firebase Admin initialized
✅ Redis connected
✅ Waktu Mode scheduler started (check every 30s)
✅ Sensor Mode monitoring started
✨ ApsGo Railway Worker is running!
```
**Jika ada error:**
- `Firebase initialization failed` → Check credentials
- `Redis error` → Pastikan Redis database sudah di-add
- `Cannot find module` → Check Root Directory setting
---
### **STEP 8: Test Penjadwalan**
**Cara 1: Set jadwal dekat waktu sekarang**
1. Buka Flutter app
2. Kontrol Waktu → Set Jadwal 1 = (waktu sekarang + 2 menit)
3. Durasi = 10 detik
4. **Toggle "Aktif" = ON**
5. Simpan
6. Tunggu 2 menit
7. Cek Railway logs
**Expected di logs:**
```
🕐 JADWAL 1 TRIGGERED: 14:30
📌 Added to queue: jadwal_1_2026-02-10_14:30
💧 Processing Job: jadwal_1_...
Type: waktu_jadwal_1
Pots: [1, 2, 3, 4, 5]
Duration: 10s
🔛 Turning ON: mosvet_1, mosvet_3, mosvet_4, mosvet_5, mosvet_6, mosvet_7
⏳ Waiting 10s...
🔚 Turning OFF: mosvet_1, mosvet_3, mosvet_4, mosvet_5, mosvet_6, mosvet_7
✅ Worker completed job jadwal_1_...
📊 History logged: 2026-02-10 14:30
```
**Cara 2: Check Firebase langsung**
1. Firebase Console → Realtime Database
2. Monitor path `/kontrol/waktu`
3. Monitor path `/aktuator/` (harus berubah dari false → true → false)
---
## 🐛 TROUBLESHOOTING CHECKLIST
### ❌ "No logs at all in Railway"
- [ ] Root directory = `railway-worker`?
- [ ] Environment variables complete?
- [ ] Redis database added?
- [ ] Check Deployments tab for build errors
### ❌ "Logs show but no schedule triggers"
- [ ] Firebase `/kontrol/waktu` = true?
- [ ] Jadwal sudah di-set di Flutter?
- [ ] Format waktu benar (HH:mm string)?
- [ ] Current time match jadwal?
### ❌ "Firebase initialization failed"
- [ ] FIREBASE_PRIVATE_KEY format benar (dengan \n)?
- [ ] SERVICE_ACCOUNT email benar?
- [ ] Database URL benar?
- [ ] Firebase Rules allow read/write?
### ❌ "Redis connection error"
- [ ] Redis database sudah di-add di Railway?
- [ ] REDIS_HOST/PORT/PASSWORD auto-injected?
---
## 📊 CARA CEK DATA FIREBASE DARI FLUTTER
Tambahkan button debug di Flutter (temporary):
```dart
ElevatedButton(
onPressed: () async {
final data = await FirebaseDatabase.instance.ref('kontrol').get();
print('KONTROL DATA: ${data.value}');
},
child: Text('Debug: Print Kontrol Data'),
)
```
Check debug console untuk melihat data yang tersimpan.
---
## 🎯 QUICK TEST SCRIPT
Jalankan dari terminal:
```powershell
# Test 1: Check Firebase data
cd f:\ApsGo\ApsGo\railway-worker
node test-firebase-connection.js
# Test 2: Check queue (if worker running)
node check-queue.js
```
---
## 📞 LANGKAH SELANJUTNYA
Setelah mengikuti STEP 1-8:
1. **Screenshot Railway Logs** yang muncul
2. **Screenshot Firebase Console** (path /kontrol/)
3. **Screenshot Railway Settings** (Root Directory & Variables)
Dengan screenshot tersebut, saya bisa bantu troubleshoot lebih lanjut jika masih ada masalah!
---
## ✅ CHECKLIST FINAL
Sebelum test:
- [ ] Data jadwal ada di Firebase `/kontrol/`
- [ ] `waktu` = true di Firebase
- [ ] Railway root directory = `railway-worker`
- [ ] 4 Firebase env variables di Railway
- [ ] Redis database added di Railway
- [ ] Worker logs muncul di Railway
- [ ] Jadwal set 2-3 menit dari sekarang untuk test
**Jika semua checked, penjadwalan PASTI jalan!** ✨

113
STATUS.md
View File

@ -1,113 +0,0 @@
# Status Proyek ApsGo
**Tanggal Update:** 11 Februari 2026
## ✅ Status Saat Ini
### Sistem Berjalan
- ✅ **Real-time automation** dengan delay 20 detik/section
- ✅ Railway Worker aktif 24/7
- ✅ Firebase Realtime Database sync
- ✅ Mobile app Flutter (Android & iOS ready)
- ✅ Auto-recovery dari timeout errors
### Konfigurasi Terkini
- **1 waktu jadwal → banyak pompa sekaligus**
- Ketika waktu penjadwalan match (contoh: 12:05)
- Semua pompa yang terkonfigurasi akan ON bersamaan
- Duration: 600 detik (10 menit) default
- Mosvets 1-7 bisa dikontrol simultan
## 🔧 Komponen Teknis
### Railway Worker
- **Repository:** https://github.com/awisnuu/myreppril.git
- **Status:** Deployed & Running
- **Features:**
- Firebase SDK + REST API fallback
- BullMQ job queue (concurrency: 1)
- Timezone: Asia/Jakarta (UTC+7)
- Health checks: Firebase, Redis, Queue
- Auto timeout handling (10s limit)
- Job-level timeout (15 min max)
### Firebase Configuration
- **Database:** project-ta-951b4-default-rtdb.firebaseio.com
- **Nodes:**
- `/kontrol` - Scheduling configuration
- `/aktuator` - Real-time device control
- `/sensor` - Sensor data
- `/history` - Activity logs
### Mobile App
- **Framework:** Flutter
- **Auth:** Firebase Auth + Google Sign-In
- **Features:**
- Manual control page
- Schedule configuration (waktu mode)
- Real-time sensor monitoring
- History viewer
## ⚠️ Known Issues
### 1. Multiple Pumps per Schedule
**Status:** By Design
- Satu waktu penjadwalan mengontrol semua pompa bersamaan
- Jika butuh kontrol terpisah, perlu implementasi:
- Jadwal terpisah per pompa
- Queue scheduling dengan interval
- Individual pump selection
### 2. Delay 20 Detik
**Status:** Normal Behavior
- Scheduler check setiap 60 detik
- Processing job: ~20 detik
- Total effective delay: 20-60 detik dari waktu target
## 📊 Performance Metrics
- **Scheduler Accuracy:** ±20 detik
- **Firebase Sync Latency:** <2 detik
- **Job Processing Time:** ~20 detik (turn ON + countdown + turn OFF)
- **Uptime:** 24/7 dengan auto-recovery
- **Error Rate:** <1% (dengan REST API fallback)
## 🚀 Repository Links
- **Main Project:** https://github.com/awisnuu/Apsgo.git
- **Railway Worker:** https://github.com/awisnuu/myreppril.git
## 📝 Dokumentasi
- [README.md](README.md) - Project overview
- [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) - Deployment instructions
- [DEBUG_TESTING_GUIDE.md](DEBUG_TESTING_GUIDE.md) - Testing procedures
- [RAILWAY_TROUBLESHOOTING.md](RAILWAY_TROUBLESHOOTING.md) - Common issues
- [railway-worker/](railway-worker/) - Worker documentation
## 🔄 Next Steps (Future Improvements)
1. **Separate Pump Scheduling**
- Individual pump control per schedule
- Queue-based sequential execution
- Configurable delays between pumps
2. **Enhanced Monitoring**
- Real-time dashboard
- Alert notifications
- Performance analytics
3. **Advanced Scheduling**
- Multiple schedules per day
- Day-of-week selection
- Holiday exceptions
4. **Sensor Integration**
- Auto-adjust based on soil moisture
- Weather API integration
- Smart duration calculation
---
**Last Updated:** 2026-02-11 13:30 WIB
**System Status:** ✅ Operational

View File

@ -7,13 +7,14 @@ plugins {
}
android {
namespace = "com.kampus.apsgo"
namespace = "com.kampus.irrigo"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
@ -22,10 +23,10 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.kampus.apsgo"
applicationId = "com.kampus.irrigo"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
@ -40,6 +41,10 @@ android {
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
flutter {
source = "../.."
}

View File

@ -1,6 +1,7 @@
{
"project_info": {
"project_number": "217854138058",
"firebase_url": "https://project-ta-951b4-default-rtdb.firebaseio.com",
"project_id": "project-ta-951b4",
"storage_bucket": "project-ta-951b4.firebasestorage.app"
},
@ -9,10 +10,18 @@
"client_info": {
"mobilesdk_app_id": "1:217854138058:android:b8bd92665b1766ad0c4633",
"android_client_info": {
"package_name": "com.kampus.apsgo"
"package_name": "com.kampus.irrigo"
}
},
"oauth_client": [
{
"client_id": "217854138058-1jj0n8jplpq5sj8b3vu6s60pne3bqkfk.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.kampus.irrigo",
"certificate_hash": "a47083645 3eb8ca78f84059dc3387f728d6eb4c3"
}
},
{
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
"client_type": 3
@ -29,6 +38,13 @@
{
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "217854138058-1kl7acuvsqkicm00t9m6qn1dkegh9r1e.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.kampus.irrigo"
}
}
]
}

View File

@ -1,8 +1,32 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Notification Permissions -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Network and Firebase -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="ApsGo"
android:label="irrigo"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- Firebase Messaging Configuration -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="irrigp_watering_channel" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@mipmap/ic_launcher" />
<!-- Foreground Service for Background Notifications -->
<service
android:name=".NotificationService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true"
@ -25,6 +49,25 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Broadcast Receiver for handling boot completed and notification callbacks -->
<!-- Using the FlutterLocalNotifications plugin's built-in receivers -->
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<!-- Notification action receiver -->
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:exported="false"
android:permission="android.permission.POST_NOTIFICATIONS" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

View File

@ -0,0 +1,21 @@
package com.kampus.irrigo
import io.flutter.embedding.android.FlutterActivity
import android.os.Build
import android.util.Log
class MainActivity: FlutterActivity() {
companion object {
private const val TAG = "IrriGoNotification"
}
override fun onStart() {
super.onStart()
Log.d(TAG, "MainActivity started - notifying notification system")
}
override fun onResume() {
super.onResume()
Log.d(TAG, "MainActivity resumed - notifying notification system")
}
}

View File

@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
flutter_analysis.txt Normal file

Binary file not shown.

View File

@ -368,7 +368,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -547,7 +547,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>ApsGo</string>
<string>IrriGo</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ApsGo</string>
<string>IrriGo</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>

View File

@ -62,7 +62,7 @@ class DefaultFirebaseOptions {
projectId: 'project-ta-951b4',
storageBucket: 'project-ta-951b4.firebasestorage.app',
databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com',
iosBundleId: 'com.kampus.apsgo',
iosBundleId: 'com.kampus.irrigo',
);
static const FirebaseOptions web = FirebaseOptions(

View File

@ -1,15 +1,23 @@
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart';
import 'screens/landing_page.dart';
import 'screens/login_page.dart';
import 'theme/app_color.dart';
import 'services/history_logging_service.dart';
import 'services/kontrol_automation_service.dart';
import 'services/notification_service.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
debugPrint('🔔 Background notification received: ${message.data}');
}
void main() {
// Start history logging service as soon as app starts
WidgetsFlutterBinding.ensureInitialized();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(const MyApp());
}
@ -40,23 +48,30 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
switch (state) {
case AppLifecycleState.resumed:
// App kembali ke foreground
print('📱 App resumed - services will auto-start when needed');
print('📱 App resumed - checking pending waterings...');
// Check any pending waterings that weren't executed (user didn't tap notification)
NotificationService.instance.checkAndExecutePendingWaterings();
break;
case AppLifecycleState.inactive:
// App temporary inactive (misal: phone call)
print('📱 App inactive');
break;
case AppLifecycleState.paused:
// App di background, stop services untuk hemat battery
print('📱 App paused - stopping background services');
// App di background
// NOTE: DO NOT stop KontrolAutomationService!
// Scheduled notifications must fire even when app is paused
// Only stop history logging to save battery
print(
'📱 App paused - background services continue running for notifications',
);
HistoryLoggingService().stop();
KontrolAutomationService().stopAll();
// KontrolAutomationService().stopAll(); // REMOVED: Breaks scheduled notifications!
break;
case AppLifecycleState.detached:
// App akan di-terminate
print('📱 App detaching - cleanup services');
HistoryLoggingService().dispose();
KontrolAutomationService().dispose();
// KontrolAutomationService().dispose(); // Keep running until truly terminated
break;
case AppLifecycleState.hidden:
// App hidden (new in Flutter 3.13+)
@ -69,7 +84,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ApsGo',
title: 'IrriGo',
theme: ThemeData(
primaryColor: AppColor.primary,
scaffoldBackgroundColor: AppColor.background,
@ -109,6 +124,15 @@ class _FirebaseInitializerState extends State<FirebaseInitializer> {
});
print('✅ Firebase initialized successfully');
await NotificationService.instance.initialize();
print('✅ Notification service initialized');
// Print diagnostic info for debugging
await NotificationService.instance.printDiagnosticInfo();
await NotificationService.instance.syncScheduleRemindersFromFirebase();
print('✅ Local schedule reminders synced');
// Start history logging service after Firebase is initialized
final loggingService = HistoryLoggingService();
loggingService.start();
@ -121,6 +145,15 @@ class _FirebaseInitializerState extends State<FirebaseInitializer> {
});
print('✅ Firebase already initialized');
await NotificationService.instance.initialize();
print('✅ Notification service initialized');
// Print diagnostic info for debugging
await NotificationService.instance.printDiagnosticInfo();
await NotificationService.instance.syncScheduleRemindersFromFirebase();
print('✅ Local schedule reminders synced');
// Start logging service
final loggingService = HistoryLoggingService();
loggingService.start();

View File

@ -5,6 +5,7 @@ import '../services/auth_service.dart';
import '../services/firebase_database_service.dart';
import 'kontrol_page.dart';
import 'histori_page.dart';
// import 'notification_debug_screen.dart';
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@ -14,6 +15,12 @@ class DashboardPage extends StatefulWidget {
}
class _DashboardPageState extends State<DashboardPage> {
String formatNumber(dynamic value, {int decimal = 1}) {
final number = double.tryParse(value.toString()) ?? 0;
return number.toStringAsFixed(decimal);
}
int _selectedIndex = 0;
final _dbService = FirebaseDatabaseService();
final _authService = AuthService();
@ -170,7 +177,7 @@ class _DashboardPageState extends State<DashboardPage> {
),
child: ClipOval(
child: Image.asset(
'assets/images/logo_apsgo.png',
'assets/images/logo_irrigo.png',
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(
@ -184,7 +191,7 @@ class _DashboardPageState extends State<DashboardPage> {
),
const SizedBox(height: 12),
Text(
'ApsGo',
'IrriGo',
style: TextStyle(
color: Colors.white,
fontSize: 20,
@ -222,6 +229,26 @@ class _DashboardPageState extends State<DashboardPage> {
title: 'Histori',
index: 2,
),
// ListTile(
// leading: Icon(
// Icons.notifications_active,
// color: Colors.orange[700],
// ),
// title: const Text(
// 'Debug Notifikasi',
// style: TextStyle(fontWeight: FontWeight.w500),
// ),
// onTap: () {
// Navigator.pop(context);
// Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) => const NotificationDebugScreen(),
// ),
// );
// },
// ),
const Divider(),
ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
@ -333,9 +360,11 @@ class _DashboardPageState extends State<DashboardPage> {
}
final data = snapshot.data!;
final suhu = data['suhu'] ?? '0';
final kelembapan = data['kelembapan'] ?? '0';
final ldr = data['ldr'] ?? '0';
// final suhu = data['suhu'] ?? '0';
// final kelembapan = data['kelembapan'] ?? '0';
final suhu = formatNumber(data['suhu'], decimal: 1);
final kelembapan = formatNumber(data['kelembapan'], decimal: 1);
final ldr = formatNumber(data['ldr'], decimal: 1);
final waterFlow = data['water_flow'] ?? 0;
final soil1 = data['soil_1'] ?? '0';
final soil2 = data['soil_2'] ?? '0';
@ -376,7 +405,7 @@ class _DashboardPageState extends State<DashboardPage> {
_buildSensorCard(
title: "Light",
value: ldr,
unit: "%",
unit: "",
icon: Icons.wb_sunny_outlined,
color: Colors.amber,
),

File diff suppressed because it is too large Load Diff

View File

@ -125,6 +125,11 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
return false;
}
if ((_pompaAir || _pompaPupuk) && _selectedPots.isEmpty) {
_showError('Tidak dapat menyimpan jadwal: Pompa aktif tanpa kran/solenoid (POT) yang terbuka.');
return false;
}
if (_durasi <= 0) {
_showError('Durasi harus lebih dari 0');
return false;
@ -247,6 +252,28 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
);
}
void _showAlertDialog(BuildContext context, {required String title, required String message}) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -518,6 +545,14 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
),
TextButton.icon(
onPressed: () {
if (_pompaAir || _pompaPupuk) {
_showAlertDialog(
context,
title: 'Bersihkan Pot',
message: 'Tidak dapat menonaktifkan seluruh kran/solenoid saat pompa masih aktif. Matikan pompa terlebih dahulu.',
);
return;
}
setState(() {
_potSelection = [false, false, false, false, false];
});
@ -536,6 +571,18 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
Widget _buildPotCheckbox(int potNumber, bool selected) {
return InkWell(
onTap: () {
final isCurrentlySelected = _potSelection[potNumber - 1];
if (isCurrentlySelected) {
final otherSelectedCount = _potSelection.where((p) => p).length - 1;
if (otherSelectedCount == 0 && (_pompaAir || _pompaPupuk)) {
_showAlertDialog(
context,
title: 'Solenoid Terakhir',
message: 'Tidak dapat menonaktifkan kran/solenoid terakhir saat pompa masih aktif. Matikan pompa terlebih dahulu.',
);
return;
}
}
setState(() {
_potSelection[potNumber - 1] = !_potSelection[potNumber - 1];
});
@ -594,7 +641,17 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
children: [
SwitchListTile(
value: _pompaAir,
onChanged: (value) => setState(() => _pompaAir = value),
onChanged: (value) {
if (value && _selectedPots.isEmpty) {
_showAlertDialog(
context,
title: 'Pompa Aktif Tanpa Solenoid',
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
);
return;
}
setState(() => _pompaAir = value);
},
title: const Text('Pompa Air'),
subtitle: const Text('Mengalirkan air'),
secondary: Icon(
@ -606,7 +663,17 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
const Divider(),
SwitchListTile(
value: _pompaPupuk,
onChanged: (value) => setState(() => _pompaPupuk = value),
onChanged: (value) {
if (value && _selectedPots.isEmpty) {
_showAlertDialog(
context,
title: 'Pompa Aktif Tanpa Solenoid',
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
);
return;
}
setState(() => _pompaPupuk = value);
},
title: const Text('Pompa Larutan Nutrisi'),
subtitle: const Text('Mengalirkan larutan nutrisi cair'),
secondary: Icon(

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../theme/app_color.dart';
import '../widgets/kontrol_widgets.dart';
import 'pot_selection_page.dart';
import 'jadwal_management_page.dart';
import 'threshold_management_page.dart';
import '../services/kontrol_storage.dart';
@ -166,6 +165,15 @@ class _ManualControlPageState extends State<ManualControlPage> {
}
Future<void> _jalankan() async {
if ((_pompaAir || _pompaNutrisi) && !_potStatus.any((p) => p)) {
_showAlertDialog(
context,
title: 'Pengaturan Tidak Valid',
message: 'Pompa aktif tanpa kran/solenoid (POT) yang terbuka. Silakan buka minimal 1 kran/solenoid terlebih dahulu.',
);
return;
}
try {
// Update Firebase with current state
await _dbService.setMultipleAktuator({
@ -186,6 +194,24 @@ class _ManualControlPageState extends State<ManualControlPage> {
pots: _potStatus,
);
// Simpan histori penyiraman manual jika ada pompa & pot aktif
if ((_pompaAir || _pompaNutrisi) && _potStatus.any((p) => p)) {
final activePotNumbers = <int>[];
for (int i = 0; i < _potStatus.length; i++) {
if (_potStatus[i]) activePotNumbers.add(i + 1);
}
// Fire-and-forget (tidak perlu await supaya UI tidak lambat)
_dbService
.saveHistoriPenyiramanManual(
potNumbers: activePotNumbers,
pompaAir: _pompaAir,
pompaNutrisi: _pompaNutrisi,
)
.catchError(
(e) => debugPrint('Gagal simpan histori manual: $e'),
);
}
// Reset flag perubahan lokal
setState(() {
_hasLocalChanges = false;
@ -223,6 +249,28 @@ class _ManualControlPageState extends State<ManualControlPage> {
}
}
void _showAlertDialog(BuildContext context, {required String title, required String message}) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
@ -237,6 +285,17 @@ class _ManualControlPageState extends State<ManualControlPage> {
title: 'Pompa Air',
isActive: _pompaAir,
onPressed: () {
if (!_pompaAir) {
final activePots = _potStatus.where((p) => p).length;
if (activePots == 0) {
_showAlertDialog(
context,
title: 'Pompa Aktif Tanpa Solenoid',
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
);
return;
}
}
setState(() {
_pompaAir = !_pompaAir;
_hasLocalChanges = true;
@ -247,6 +306,17 @@ class _ManualControlPageState extends State<ManualControlPage> {
title: 'Pompa Nutrisi',
isActive: _pompaNutrisi,
onPressed: () {
if (!_pompaNutrisi) {
final activePots = _potStatus.where((p) => p).length;
if (activePots == 0) {
_showAlertDialog(
context,
title: 'Pompa Aktif Tanpa Solenoid',
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
);
return;
}
}
setState(() {
_pompaNutrisi = !_pompaNutrisi;
_hasLocalChanges = true;
@ -286,6 +356,18 @@ class _ManualControlPageState extends State<ManualControlPage> {
title: 'POT ${index + 1}',
isActive: _potStatus[index],
onPressed: () {
final isCurrentlyActive = _potStatus[index];
if (isCurrentlyActive) {
final activePots = _potStatus.where((p) => p).length;
if (activePots == 1 && (_pompaAir || _pompaNutrisi)) {
_showAlertDialog(
context,
title: 'Solenoid Terakhir',
message: 'Tidak dapat menonaktifkan kran/solenoid terakhir saat pompa masih aktif. Matikan pompa terlebih dahulu.',
);
return;
}
}
setState(() {
_potStatus[index] = !_potStatus[index];
_hasLocalChanges = true;

View File

@ -83,7 +83,7 @@ class _LandingPageState extends State<LandingPage> {
),
child: ClipOval(
child: Image.asset(
'assets/images/logo_apsgo.png',
'assets/images/logo_irrigo.png',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.eco, size: 60, color: AppColor.primary);
@ -93,7 +93,7 @@ class _LandingPageState extends State<LandingPage> {
),
const SizedBox(height: 40),
Text(
"ApsGo",
"IrriGo",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
@ -102,7 +102,7 @@ class _LandingPageState extends State<LandingPage> {
),
const SizedBox(height: 16),
Text(
"Kelola Hidupmu\nDengan Lebih Mudah",
"Siram Tanamanmu\nDengan Lebih Pintar",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
@ -113,7 +113,7 @@ class _LandingPageState extends State<LandingPage> {
),
const SizedBox(height: 12),
Text(
"Semua yang kamu butuhkan dalam satu aplikasi",
"Otomatisasi penyiraman untuk menjaga tanaman tetap segar dan sehat setiap hari",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
@ -121,7 +121,7 @@ class _LandingPageState extends State<LandingPage> {
height: 1.4,
),
),
const SizedBox(height: 60),
const SizedBox(height: 40),
// Tombol Next
SizedBox(
width: double.infinity,
@ -148,6 +148,8 @@ class _LandingPageState extends State<LandingPage> {
),
),
const Spacer(),
_buildCollaborationFooter(),
const SizedBox(height: 16),
],
),
);
@ -175,7 +177,7 @@ class _LandingPageState extends State<LandingPage> {
),
const SizedBox(height: 40),
Text(
"Siap Memulai?",
"Siap Merawat Tanamanmu?",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
@ -184,7 +186,7 @@ class _LandingPageState extends State<LandingPage> {
),
const SizedBox(height: 16),
Text(
"Bergabunglah Dengan Kami\nDan Rasakan Kemudahannya",
"Pantau kelembapan tanah dan atur jadwal penyiraman dalam satu genggaman",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
@ -195,7 +197,7 @@ class _LandingPageState extends State<LandingPage> {
),
const SizedBox(height: 12),
Text(
"Daftar sekarang dan mulai perjalananmu",
"Mulai otomatisasi sekarang untuk hasil panen terbaik",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
@ -203,7 +205,7 @@ class _LandingPageState extends State<LandingPage> {
height: 1.4,
),
),
const SizedBox(height: 60),
const SizedBox(height: 40),
// Tombol Get Started
SizedBox(
width: double.infinity,
@ -221,14 +223,112 @@ class _LandingPageState extends State<LandingPage> {
),
),
child: const Text(
"Get Started",
"Mulai Dari Sekarang",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
const Spacer(),
_buildCollaborationFooter(),
const SizedBox(height: 16),
],
),
);
}
Widget _buildCollaborationFooter() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Kampus Logo (Politeknik Negeri Jember)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/logo_polije.png',
height: 24,
errorBuilder: (context, error, stackTrace) {
// Fallback to existing asset logo_apsgo.png
return Image.asset(
'assets/images/logo_poliyey.png',
height: 24,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.school,
size: 20,
color: AppColor.primary.withOpacity(0.7),
);
},
);
},
),
const SizedBox(width: 8),
const Text(
"Politeknik Negeri Jember",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColor.textDark,
),
),
],
),
const SizedBox(width: 12),
Container(
height: 16,
width: 1,
color: AppColor.textDark.withOpacity(0.2),
),
const SizedBox(width: 12),
// Partner Logo (Mitra)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/logo_mitra.png',
height: 24,
errorBuilder: (context, error, stackTrace) {
// Fallback to existing asset logo_irrigo.png
return Image.asset(
'assets/images/logo_mitra.png',
height: 24,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.handshake,
size: 20,
color: AppColor.primary.withOpacity(0.7),
);
},
);
},
),
const SizedBox(width: 8),
const Text(
"YUSUF BIBIT",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColor.textDark,
),
),
],
),
],
),
const SizedBox(height: 6),
Text(
"by: Tugas Akhir",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColor.primary.withOpacity(0.8),
letterSpacing: 0.5,
),
),
],
);
}
}

View File

@ -95,33 +95,6 @@ class _LoginPageState extends State<LoginPage> {
);
}
Future<void> _handleGoogleSignIn() async {
setState(() {
_isLoading = true;
});
try {
await _authService.signInWithGoogle();
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const DashboardPage()),
);
}
} catch (e) {
if (mounted) {
_showError(e.toString());
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
@ -158,7 +131,7 @@ class _LoginPageState extends State<LoginPage> {
),
child: ClipOval(
child: Image.asset(
'assets/images/logo_apsgo.png',
'assets/images/logo_irrigo.png',
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.eco, size: 50, color: AppColor.primary);
@ -168,7 +141,7 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: 24),
Text(
"Sign In",
"Login",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -230,47 +203,6 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: 24),
// Divider with "or login with"
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
"or login with",
style: TextStyle(color: Colors.grey[600]),
),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 24),
// Google Sign In Button
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.all(14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide(color: Colors.grey[300]!),
),
onPressed: _isLoading ? null : _handleGoogleSignIn,
icon: const Icon(
Icons.g_mobiledata,
size: 32,
color: Colors.red,
),
label: const Text(
"Continue with Google",
style: TextStyle(color: Colors.black87, fontSize: 16),
),
),
),
const SizedBox(height: 24),
// Register Link
Center(
child: TextButton(
@ -282,11 +214,11 @@ class _LoginPageState extends State<LoginPage> {
},
child: RichText(
text: TextSpan(
text: "Don't have an account? ",
text: "Belum punya akun? ",
style: TextStyle(color: Colors.grey[600]),
children: [
TextSpan(
text: "Register",
text: "Daftar",
style: TextStyle(
color: AppColor.primary,
fontWeight: FontWeight.bold,

View File

@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../services/notification_service.dart';
class NotificationDebugScreen extends StatefulWidget {
const NotificationDebugScreen({super.key});
@override
State<NotificationDebugScreen> createState() =>
_NotificationDebugScreenState();
}
class _NotificationDebugScreenState extends State<NotificationDebugScreen> {
bool _isLoading = false;
bool? _canScheduleExactNotifications;
String? _lastStatus;
String? _fcmToken;
List<PendingNotificationRequest> _pendingNotifications = [];
@override
void initState() {
super.initState();
_loadPendingNotifications();
}
Future<void> _setStatus(String message) async {
if (!mounted) return;
setState(() {
_lastStatus = message;
});
}
Future<void> _loadPendingNotifications() async {
setState(() {
_isLoading = true;
});
try {
await NotificationService.instance.initialize();
final canScheduleExact =
await NotificationService.instance.canScheduleExactNotifications();
final pending = await NotificationService.instance.pendingNotifications();
final token = await NotificationService.instance.getFcmToken();
if (!mounted) return;
setState(() {
_pendingNotifications = pending;
_canScheduleExactNotifications = canScheduleExact;
_fcmToken = token;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
_lastStatus = 'Gagal memuat notifikasi tertunda: $e';
});
}
}
Future<void> _sendImmediateSimple() async {
await _runAction(() async {
await NotificationService.instance.showInstantNotification(
title: 'Tes Notifikasi IrriGo',
body: 'Notifikasi lokal berhasil dikirim dari halaman debug.',
payload: 'debug:immediate',
);
}, 'Notifikasi langsung terkirim');
}
Future<void> _sendModeChangedSample() async {
await _runAction(() async {
await NotificationService.instance.showModeChangedNotification(
modeLabel: 'berdasarkan jadwal',
enabled: true,
);
}, 'Notifikasi mode berhasil dikirim');
}
Future<void> _sendSensorTriggeredSample() async {
await _runAction(() async {
await NotificationService.instance.showWateringNotification(
title: 'IrriGo - Penyiraman Otomatis',
body:
'Penyiraman dilakukan pada pot 3 karena kelembapan 28% di bawah ambang batas 40%.',
payload: {
'type': 'sensor_triggered',
'pot': 3,
'soil': 28,
'threshold': 40,
},
);
}, 'Notifikasi otomatis sensor terkirim');
}
Future<void> _scheduleThirtySecondNotification() async {
await _runAction(() async {
final success = await NotificationService.instance
.scheduleStable30SecondNotification(
title: 'Tes Notifikasi 30 Detik',
body:
'Notifikasi ini dijadwalkan 30 detik. Minimize app atau tutup setelah ini untuk test background notification!',
payload: 'debug:scheduled_30s',
);
if (success) {
await _setStatus(
'✅ Notifikasi 30 detik dijadwalkan STABIL! Cek ulang dibawah utk lihat status exact alarm dan pending notifikasi.',
);
} else {
await _setStatus(
'❌ GAGAL scheduling 30 detik. Periksa POST_NOTIFICATIONS permission di settings app!',
);
}
}, 'Jadwalkan Notif 30 Detik Stabil');
await _loadPendingNotifications();
}
Future<void> _requestExactAlarmPermission() async {
await _runAction(() async {
final granted =
await NotificationService.instance.requestExactAlarmsPermission();
if (!granted) {
await _setStatus(
'Exact alarm belum aktif/diizinkan oleh sistem. Aplikasi pakai inexact agar tetap stabil.',
);
return;
}
await _setStatus('Exact alarm aktif. Jadwal lokal akan lebih presisi.');
}, 'Izin exact alarm berhasil diminta');
await _loadPendingNotifications();
}
Future<void> _revalidatePermissions() async {
await _runAction(() async {
final postNotifGranted =
await NotificationService.instance.revalidatePermissions();
final exactGranted =
await NotificationService.instance.canScheduleExactNotifications();
final status = '''
Permission Status Fresh Check:
POST_NOTIFICATIONS: ${postNotifGranted ? '✅ GRANTED' : '❌ DENIED'}
SCHEDULE_EXACT_ALARM: ${exactGranted ? '✅ AVAILABLE' : '⚠️ NOT AVAILABLE'}
Jika ada yang DENIED, settings app IrriGo Izin aktifkan permission.
''';
await _setStatus(status);
}, 'Permission berhasil di-revalidasi');
await _loadPendingNotifications();
}
Future<void> _cancelAllNotifications() async {
await _runAction(() async {
await NotificationService.instance.cancelAll();
}, 'Semua notifikasi dibatalkan');
await _loadPendingNotifications();
}
Future<void> _runAction(
Future<void> Function() action,
String successText,
) async {
setState(() {
_isLoading = true;
});
try {
await NotificationService.instance.initialize();
await action();
await _setStatus(successText);
} catch (e) {
await _setStatus('Error: $e');
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Debug Notifikasi'),
backgroundColor: Colors.orange,
actions: [
IconButton(
onPressed: _isLoading ? null : _loadPendingNotifications,
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
),
],
),
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildInfoCard(),
const SizedBox(height: 16),
_buildActionCard(),
const SizedBox(height: 16),
_buildPendingCard(),
if (_lastStatus != null) ...[
const SizedBox(height: 16),
_buildStatusCard(),
],
],
),
),
);
}
Widget _buildInfoCard() {
return Card(
elevation: 2,
color: Colors.blue.shade50,
child: const Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Cara kerja notifikasi IrriGo',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'• Notifikasi langsung muncul saat app aktif atau background.\n'
'• Notifikasi terjadwal lokal tetap bisa muncul walau app diminimize/ditutup setelah dijadwalkan.\n'
'• Agar jadwal presisi, aktifkan exact alarm.\n'
'• Untuk event server saat app benar-benar mati, gunakan push notification dari Railway Worker + FCM (sudah dipasang).',
style: TextStyle(fontSize: 13, height: 1.4),
),
],
),
),
);
}
Widget _buildActionCard() {
return Card(
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Aksi Debug',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: _isLoading ? null : _sendImmediateSimple,
icon: const Icon(Icons.notifications_active),
label: const Text('Kirim Notifikasi Langsung'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _isLoading ? null : _sendModeChangedSample,
icon: const Icon(Icons.settings),
label: const Text('Simulasi Notif Ubah Mode'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _isLoading ? null : _sendSensorTriggeredSample,
icon: const Icon(Icons.water_drop),
label: const Text('Simulasi Notif Sensor Trigger'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _isLoading ? null : _scheduleThirtySecondNotification,
icon: const Icon(Icons.schedule),
label: const Text('Jadwalkan Notif 30 Detik'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _isLoading ? null : _requestExactAlarmPermission,
icon: const Icon(Icons.alarm_on),
label: const Text('Aktifkan Exact Alarm'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _isLoading ? null : _loadPendingNotifications,
icon: const Icon(Icons.key),
label: const Text('Cek Ulang Token FCM'),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _isLoading ? null : _revalidatePermissions,
icon: const Icon(Icons.verified_user),
label: const Text('Validasi Ulang Permission'),
),
const SizedBox(height: 8),
TextButton.icon(
onPressed: _isLoading ? null : _cancelAllNotifications,
icon: const Icon(Icons.cancel),
label: const Text('Batalkan Semua Notifikasi'),
),
],
),
),
);
}
Widget _buildPendingCard() {
return Card(
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Notifikasi Tertunda',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'Exact alarm: ${_canScheduleExactNotifications == null ? 'memuat...' : (_canScheduleExactNotifications! ? 'aktif' : 'belum aktif')}',
style: TextStyle(
fontSize: 13,
color:
_canScheduleExactNotifications == true
? Colors.green.shade700
: Colors.red.shade700,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
SelectableText(
'FCM token: ${_fcmToken == null ? 'belum tersedia' : _fcmToken!}',
style: TextStyle(
fontSize: 12,
color:
_fcmToken == null
? Colors.red.shade700
: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
if (_pendingNotifications.isEmpty)
const Text('Belum ada notifikasi terjadwal.')
else
..._pendingNotifications.map(
(notification) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(notification.title ?? '(tanpa judul)'),
const SizedBox(height: 4),
Text(notification.body ?? '(tanpa isi)'),
const SizedBox(height: 4),
Text(
'ID: ${notification.id}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
),
),
],
),
),
);
}
Widget _buildStatusCard() {
final success =
_lastStatus!.contains('terkirim') ||
_lastStatus!.contains('dijadwalkan') ||
_lastStatus!.contains('dibatalkan');
return Card(
color: success ? Colors.green.shade50 : Colors.red.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
_lastStatus!,
style: TextStyle(
fontWeight: FontWeight.w600,
color: success ? Colors.green.shade800 : Colors.red.shade800,
),
),
),
);
}
}

View File

@ -4,6 +4,7 @@ import '../widgets/kontrol_widgets.dart';
import '../services/kontrol_storage.dart';
import '../services/firebase_database_service.dart';
import '../services/kontrol_automation_service.dart';
import '../services/notification_service.dart';
class SensorConfigPage extends StatefulWidget {
final String potName;
@ -222,6 +223,11 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
setState(() => _isSensorModeActive = value);
try {
await _dbService.setOtomatis(value);
await NotificationService.instance
.showModeChangedNotification(
modeLabel: 'otomatis berbasis sensor',
enabled: value,
);
if (value) {
_automationService.startSensorMode();
} else {

View File

@ -98,6 +98,11 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
return false;
}
if ((_pompaAir || _pompaPupuk) && _selectedPots.isEmpty) {
_showError('Tidak dapat menyimpan threshold: Pompa aktif tanpa kran/solenoid (POT) yang terbuka.');
return false;
}
if (_batasBawah >= _batasAtas) {
_showError('Batas bawah harus lebih kecil dari batas atas');
return false;
@ -179,6 +184,28 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
);
}
void _showAlertDialog(BuildContext context, {required String title, required String message}) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -337,25 +364,41 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
const SizedBox(height: 16),
// Smart Mode Toggle
SwitchListTile(
value: _smartMode,
onChanged: (value) {
setState(() {
_smartMode = value;
});
},
title: Text(
_smartMode ? 'Smart Mode' : 'Fixed Duration',
style: const TextStyle(fontWeight: FontWeight.w600),
// SwitchListTile(
// value: _smartMode,
// onChanged: (value) {
// setState(() {
// _smartMode = value;
// });
// },
// title: Text(
// _smartMode ? 'Smart Mode' : 'Fixed Duration',
// style: const TextStyle(fontWeight: FontWeight.w600),
// ),
// subtitle: Text(
// _smartMode
// ? 'Siram sampai kelembaban ${_batasAtas.toInt()}%'
// : 'Siram dengan durasi tetap',
// style: const TextStyle(fontSize: 12),
// ),
// activeColor: AppColor.primary,
// contentPadding: EdgeInsets.zero,
// ),
ListTile(
title: const Text(
'Smart Mode',
style: TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
_smartMode
? 'Siram sampai kelembaban ${_batasAtas.toInt()}%'
: 'Siram dengan durasi tetap',
'Siram sampai kelembaban ${_batasAtas.toInt()}%',
style: const TextStyle(fontSize: 12),
),
activeColor: AppColor.primary,
contentPadding: EdgeInsets.zero,
// contentPadding: EdgeInsets.zero,
// trailing: Switch(
// value: _smartMode,
// onChanged: null, // Disabled
// activeColor: AppColor.primary,
// ),
),
// Duration input (hanya jika Fixed Duration)
@ -440,6 +483,17 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
label: Text('Pot $potNumber'),
selected: isSelected,
onSelected: (selected) {
if (!selected) {
final selectedCount = _potSelection.where((p) => p).length;
if (selectedCount == 1 && (_pompaAir || _pompaPupuk)) {
_showAlertDialog(
context,
title: 'Solenoid Terakhir',
message: 'Tidak dapat menonaktifkan kran/solenoid terakhir saat pompa masih aktif. Matikan pompa terlebih dahulu.',
);
return;
}
}
setState(() {
_potSelection[index] = selected;
});
@ -472,6 +526,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
const SizedBox(width: 8),
TextButton.icon(
onPressed: () {
if (_pompaAir || _pompaPupuk) {
_showAlertDialog(
context,
title: 'Hapus Semua Pot',
message: 'Tidak dapat menonaktifkan seluruh kran/solenoid saat pompa masih aktif. Matikan pompa terlebih dahulu.',
);
return;
}
setState(() {
_potSelection = [false, false, false, false, false];
});
@ -515,6 +577,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
SwitchListTile(
value: _pompaAir,
onChanged: (value) {
if (value && _selectedPots.isEmpty) {
_showAlertDialog(
context,
title: 'Pompa Aktif Tanpa Solenoid',
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
);
return;
}
setState(() {
_pompaAir = value;
});
@ -528,6 +598,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
SwitchListTile(
value: _pompaPupuk,
onChanged: (value) {
if (value && _selectedPots.isEmpty) {
_showAlertDialog(
context,
title: 'Pompa Aktif Tanpa Solenoid',
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
);
return;
}
setState(() {
_pompaPupuk = value;
});
@ -539,14 +617,9 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
activeColor: AppColor.primary,
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
value: _pompaPengaduk,
onChanged: (value) {
setState(() {
_pompaPengaduk = value;
});
},
value: false,
onChanged: null, // <- tanpa ()
title: const Text('Pompa Pengaduk'),
subtitle: const Text(
'Aktifkan pompa pengaduk untuk mengaduk larutan',

View File

@ -4,6 +4,7 @@ import '../widgets/kontrol_widgets.dart';
import '../services/kontrol_storage.dart';
import '../services/firebase_database_service.dart';
import '../services/kontrol_automation_service.dart';
import '../services/notification_service.dart';
class WaktuConfigPage extends StatefulWidget {
final String potName;
@ -242,6 +243,11 @@ class _WaktuConfigPageState extends State<WaktuConfigPage> {
await _dbService.updateKontrolConfig({
'waktu': value,
});
await NotificationService.instance
.showModeChangedNotification(
modeLabel: 'berdasarkan jadwal',
enabled: value,
);
if (value) {
_automationService.startWaktuMode();
} else {

View File

@ -1,4 +1,6 @@
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/foundation.dart';
class FirebaseDatabaseService {
static final FirebaseDatabaseService _instance =
@ -139,7 +141,8 @@ class FirebaseDatabaseService {
/// Get real-time stream of kontrol configuration
Stream<Map<String, dynamic>> getKontrolStream() {
return _database.child('kontrol').onValue.map((event) {
// Check kontrol_1 dulu (data terbaru)
return _database.child('kontrol_1').onValue.map((event) {
if (event.snapshot.value != null) {
return Map<String, dynamic>.from(event.snapshot.value as Map);
}
@ -147,24 +150,35 @@ class FirebaseDatabaseService {
});
}
/// Get kontrol configuration once
/// Get kontrol configuration once (checks kontrol_1 first, then kontrol for legacy)
Future<Map<String, dynamic>> getKontrolConfig() async {
try {
// Try kontrol_1 first (current data structure)
final snapshot1 = await _database.child('kontrol_1').get();
if (snapshot1.exists && snapshot1.value != null) {
print('[DEBUG] Got kontrol config from /kontrol_1');
return Map<String, dynamic>.from(snapshot1.value as Map);
}
// Fallback to kontrol (legacy)
final snapshot = await _database.child('kontrol').get();
if (snapshot.exists) {
if (snapshot.exists && snapshot.value != null) {
print('[DEBUG] Got kontrol config from /kontrol (legacy)');
return Map<String, dynamic>.from(snapshot.value as Map);
}
print('[WARN] Kontrol config not found in both /kontrol_1 and /kontrol');
} catch (e) {
print('Error getting kontrol config: $e');
}
return {};
}
/// Update kontrol configuration
/// Update kontrol configuration (writes to kontrol_1)
Future<void> updateKontrolConfig(Map<String, dynamic> config) async {
try {
print('🔥 [DEBUG] Updating Firebase kontrol config: $config');
await _database.child('kontrol').update(config);
print('🔥 [DEBUG] Updating Firebase kontrol config (kontrol_1): $config');
await _database.child('kontrol_1').update(config);
print('✅ [DEBUG] Firebase kontrol config updated successfully');
} catch (e) {
print('❌ [DEBUG] Error updating kontrol config: $e');
@ -172,20 +186,20 @@ class FirebaseDatabaseService {
}
}
/// Set batas threshold (batas_atas, batas_bawah)
/// Set batas threshold (batas_atas, batas_bawah) - writes to kontrol_1
Future<void> setThreshold({int? batasAtas, int? batasBawah}) async {
try {
final updates = <String, dynamic>{};
if (batasAtas != null) updates['batas_atas'] = batasAtas;
if (batasBawah != null) updates['batas_bawah'] = batasBawah;
await _database.child('kontrol').update(updates);
await _database.child('kontrol_1').update(updates);
} catch (e) {
print('Error setting threshold: $e');
rethrow;
}
}
/// Set waktu penyiraman
/// Set waktu penyiraman - writes to kontrol_1
Future<void> setWaktuPenyiraman({
String? waktu1,
String? waktu2,
@ -196,30 +210,30 @@ class FirebaseDatabaseService {
if (waktu1 != null) updates['waktu_1'] = waktu1;
if (waktu2 != null) updates['waktu_2'] = waktu2;
if (waktuEnabled != null) updates['waktu'] = waktuEnabled;
await _database.child('kontrol').update(updates);
await _database.child('kontrol_1').update(updates);
} catch (e) {
print('Error setting waktu: $e');
rethrow;
}
}
/// Set durasi penyiraman
/// Set durasi penyiraman - writes to kontrol_1
Future<void> setDurasi({int? durasi1, int? durasi2}) async {
try {
final updates = <String, dynamic>{};
if (durasi1 != null) updates['durasi_1'] = durasi1;
if (durasi2 != null) updates['durasi_2'] = durasi2;
await _database.child('kontrol').update(updates);
await _database.child('kontrol_1').update(updates);
} catch (e) {
print('Error setting durasi: $e');
rethrow;
}
}
/// Toggle mode otomatis
/// Toggle mode otomatis - writes to kontrol_1
Future<void> setOtomatis(bool value) async {
try {
await _database.child('kontrol/otomatis').set(value);
await _database.child('kontrol_1/otomatis').set(value);
} catch (e) {
print('Error setting otomatis: $e');
rethrow;
@ -248,54 +262,143 @@ class FirebaseDatabaseService {
// ==================== HISTORY LOGGING ====================
/// Save sensor data snapshot to history
/// Save sensor data snapshot to history (monitoring)
Future<void> saveHistory(Map<String, dynamic> sensorData) async {
try {
final now = DateTime.now();
final dateKey =
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
final timeKey =
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
await _database.child('history/$dateKey/$timeKey').set({
...sensorData,
'timestamp': now.millisecondsSinceEpoch,
});
// Simpan per-pot menggunakan push() dengan jenis_histori: monitoring
for (int pot = 1; pot <= 5; pot++) {
final soilKey = 'soil_$pot';
final soilValue = sensorData[soilKey];
if (soilValue == null) continue;
await _database.child('history').push().set({
'timestamp': now.millisecondsSinceEpoch,
'tanggal':
'${now.day}/${now.month}/${now.year}',
'waktu':
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}',
'jenis_histori': 'monitoring',
'sumber': 'app',
'mode': '-',
'pot': pot,
'suhu': sensorData['suhu'] ?? 0,
'kelembapan': sensorData['kelembapan'] ?? 0,
'soil': soilValue,
'durasi': 0,
'type': 'app_log',
});
}
} catch (e) {
print('Error saving history: $e');
}
}
/// Get history data for a specific date range
/// Save penyiraman manual ke histori
Future<void> saveHistoriPenyiramanManual({
required List<int> potNumbers,
required bool pompaAir,
required bool pompaNutrisi,
}) async {
try {
final now = DateTime.now();
final sensorData = await getSensorData();
for (final pot in potNumbers) {
final soilKey = 'soil_$pot';
await _database.child('history').push().set({
'timestamp': now.millisecondsSinceEpoch,
'tanggal': '${now.day}/${now.month}/${now.year}',
'waktu':
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}',
'jenis_histori': 'penyiraman',
'sumber': 'app',
'mode': 'manual',
'pot': pot,
'suhu': sensorData['suhu'] ?? 0,
'kelembapan': sensorData['kelembapan'] ?? 0,
'soil': sensorData[soilKey] ?? 0,
'durasi': 0,
'type': 'manual',
'pompa_air': pompaAir,
'pompa_nutrisi': pompaNutrisi,
});
}
print('✅ Histori penyiraman manual saved: ${potNumbers.length} pot');
} catch (e) {
print('Error saving manual watering history: $e');
rethrow;
}
}
/// Get history data for a specific date range.
/// Mendukung dua format:
/// 1. Push-key flat entries: /history/-key/ (ada field `timestamp`)
/// 2. Date-keyed nested entries: /history/YYYY-MM-DD/HH:mm/ (format ESP32 lama)
Future<Map<String, dynamic>> getHistoryByDateRange(
DateTime startDate,
DateTime endDate,
) async {
final result = <String, dynamic>{};
try {
final allHistory = <String, dynamic>{};
// Query 1: Push-key entries dengan field `timestamp`
final startTs = startDate.millisecondsSinceEpoch;
final endTs =
endDate
.copyWith(hour: 23, minute: 59, second: 59)
.millisecondsSinceEpoch;
// Loop through each day in the range
for (
var date = startDate;
date.isBefore(endDate.add(const Duration(days: 1)));
date = date.add(const Duration(days: 1))
) {
final dateKey =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
final tsSnapshot =
await _database
.child('history')
.orderByChild('timestamp')
.startAt(startTs)
.endAt(endTs)
.get();
final snapshot = await _database.child('history/$dateKey').get();
if (snapshot.exists) {
allHistory[dateKey] = snapshot.value;
}
if (tsSnapshot.exists && tsSnapshot.value != null) {
final raw = Map<String, dynamic>.from(tsSnapshot.value as Map);
result.addAll(raw);
debugPrint('[History] Push-key entries fetched: ${raw.length}');
}
return allHistory;
} catch (e) {
print('Error getting history: $e');
return {};
debugPrint('Error fetching push-key history: $e');
}
try {
// Query 2: Date-keyed entries (format ESP32: YYYY-MM-DD/HH:mm)
// Iterasi setiap tanggal dalam rentang
DateTime cursor = DateTime(startDate.year, startDate.month, startDate.day);
final endDay = DateTime(endDate.year, endDate.month, endDate.day);
while (!cursor.isAfter(endDay)) {
final dateStr =
'${cursor.year}-'
'${cursor.month.toString().padLeft(2, '0')}-'
'${cursor.day.toString().padLeft(2, '0')}';
final daySnapshot = await _database.child('history/$dateStr').get();
if (daySnapshot.exists && daySnapshot.value != null) {
// Simpan dengan prefix 'date|' agar bisa dikenali oleh parser
result['date|$dateStr'] = daySnapshot.value;
debugPrint('[History] Date-keyed entries for $dateStr fetched');
}
cursor = cursor.add(const Duration(days: 1));
}
} catch (e) {
debugPrint('Error fetching date-keyed history: $e');
}
debugPrint('[History] Total raw keys returned: ${result.length}');
return result;
}
/// Get latest history entries (limit)
Future<Map<String, dynamic>> getLatestHistory({int limit = 100}) async {
try {

View File

@ -23,27 +23,9 @@ class HistoryLoggingService {
/// Start history logging service
void start({Duration? interval}) {
if (_isActive) {
debugPrint('📊 History logging already active');
return;
}
if (interval != null) {
_loggingInterval = interval;
}
_isActive = true;
debugPrint(
'📊 History logging started (interval: ${_loggingInterval.inMinutes} min)',
);
// Log immediately on start
_logCurrentData();
// Then log periodically
_loggingTimer = Timer.periodic(_loggingInterval, (timer) {
_logCurrentData();
});
// Disabled on mobile app to run exclusively on Railway/Worker
debugPrint('📊 History logging is disabled on mobile app (runs on Railway worker instead)');
return;
}
/// Stop history logging service

View File

@ -1,5 +1,6 @@
import 'package:firebase_database/firebase_database.dart';
import '../models/jadwal_model.dart';
import 'notification_service.dart';
/// Service untuk manage Jadwal di Firebase
class JadwalService {
@ -64,6 +65,9 @@ class JadwalService {
Future<bool> saveJadwal(JadwalModel jadwal) async {
try {
await _kontrolRef.child(jadwal.id).set(jadwal.toJson());
// Sync both reminder notifications and watering execution schedules
await NotificationService.instance.syncScheduleRemindersFromFirebase();
await NotificationService.instance.scheduleAllWateringExecutions();
print('✅ Jadwal ${jadwal.id} saved successfully');
return true;
} catch (e) {
@ -76,6 +80,9 @@ class JadwalService {
Future<bool> deleteJadwal(String jadwalId) async {
try {
await _kontrolRef.child(jadwalId).remove();
// Sync both reminder notifications and watering execution schedules
await NotificationService.instance.syncScheduleRemindersFromFirebase();
await NotificationService.instance.scheduleAllWateringExecutions();
print('✅ Jadwal $jadwalId deleted successfully');
return true;
} catch (e) {
@ -88,6 +95,9 @@ class JadwalService {
Future<bool> toggleJadwalAktif(String jadwalId, bool aktif) async {
try {
await _kontrolRef.child(jadwalId).child('aktif').set(aktif);
// Sync both reminder notifications and watering execution schedules
await NotificationService.instance.syncScheduleRemindersFromFirebase();
await NotificationService.instance.scheduleAllWateringExecutions();
print('✅ Jadwal $jadwalId set to ${aktif ? "aktif" : "nonaktif"}');
return true;
} catch (e) {
@ -135,7 +145,22 @@ class JadwalService {
Future<bool> setWaktuModeStatus(bool enabled) async {
try {
await _kontrolRef.child('waktu').set(enabled);
// Sync reminder notifications
await NotificationService.instance.syncScheduleRemindersFromFirebase();
// Schedule/cancel watering executions based on mode
if (enabled) {
await NotificationService.instance.scheduleAllWateringExecutions();
} else {
await NotificationService.instance.cancelAllWateringSchedules();
}
print('✅ Waktu mode set to ${enabled ? "enabled" : "disabled"}');
await NotificationService.instance.showModeChangedNotification(
modeLabel: 'berdasarkan jadwal',
enabled: enabled,
);
return true;
} catch (e) {
print('❌ Error setting waktu mode: $e');

View File

@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import 'firebase_database_service.dart';
import 'automation_constants.dart';
import 'connection_monitor_service.dart';
import 'notification_service.dart';
/// Service untuk menghandle logika otomatis kontrol waktu dan sensor
/// Berjalan di background untuk monitoring dan eksekusi otomatis
@ -31,24 +32,29 @@ class KontrolAutomationService {
// ==================== KONTROL WAKTU ====================
/// Start monitoring waktu mode
/// Cek setiap menit apakah ada jadwal yang harus dijalankan
/// Uses NotificationService exact alarm scheduling instead of polling
/// This eliminates dependency on worker and timer-based checking
void startWaktuMode() {
// Disabled on mobile app to let Railway worker handle all scheduling/watering
debugPrint('🕐 Waktu Mode: Disabled on mobile app (handled by Railway worker instead)');
if (DateTime.now().year > 2000) return; // Early exit to prevent background scheduling/watering in app
if (_isWaktuModeActive) return;
// Start connection monitor
// Start connection monitor for sensor fallback
_connectionMonitor.start();
_isWaktuModeActive = true;
debugPrint('🕐 Waktu Mode: Started');
debugPrint('🕐 Waktu Mode: Started (using exact alarm scheduling)');
// Cek setiap 30 detik (gunakan constant)
_waktuCheckTimer = Timer.periodic(
Duration(seconds: AutomationConstants.waktuCheckInterval),
(timer) => _checkScheduledWatering(),
// Setup callback to handle watering execution when alarm fires
NotificationService.setOnWateringScheduleCallback(
_handleWateringScheduleExecution,
);
// Jalankan check pertama kali
_checkScheduledWatering();
// Schedule all watering executions using exact alarm
// This will fire even when app is minimized/killed
_scheduleWateringExecutions();
}
/// Stop monitoring waktu mode
@ -56,84 +62,72 @@ class KontrolAutomationService {
_waktuCheckTimer?.cancel();
_waktuCheckTimer = null;
_isWaktuModeActive = false;
// Cancel all watering schedule executions
_cancelWateringSchedules();
// Clear callback
NotificationService.setOnWateringScheduleCallback(null);
debugPrint('🕐 Waktu Mode: Stopped');
}
/// Check apakah ada jadwal penyiraman yang harus dijalankan
Future<void> _checkScheduledWatering() async {
/// Schedule all watering executions from Firebase
/// Called when waktu mode starts or when jadwal changes
Future<void> _scheduleWateringExecutions() async {
try {
// Check connection first
if (!_connectionMonitor.isConnected) {
debugPrint('⚠️ No Firebase connection, skipping schedule check');
return;
}
final kontrolConfig = await _dbService.getKontrolConfig();
final waktuEnabled = kontrolConfig['waktu'] ?? false;
if (!waktuEnabled || !_isWaktuModeActive) return;
final now = DateTime.now();
final currentTime =
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
final waktu1 = kontrolConfig['waktu_1'] ?? '';
final waktu2 = kontrolConfig['waktu_2'] ?? '';
final durasi1 =
kontrolConfig['durasi_1'] ?? AutomationConstants.defaultDurasiDetik;
final durasi2 =
kontrolConfig['durasi_2'] ?? AutomationConstants.defaultDurasiDetik;
// Check Jadwal 1
if (waktu1.isNotEmpty &&
_shouldStartWatering('jadwal_1', currentTime, waktu1)) {
debugPrint('🕐 Executing Jadwal 1 at $currentTime');
await _executeWatering(
scheduleId: 'jadwal_1',
pompaAir: true,
pompaPupuk: true,
pots: [1, 2, 3, 4, 5], // Semua pot
durasiDetik: durasi1,
);
}
// Check Jadwal 2
if (waktu2.isNotEmpty &&
_shouldStartWatering('jadwal_2', currentTime, waktu2)) {
debugPrint('🕐 Executing Jadwal 2 at $currentTime');
await _executeWatering(
scheduleId: 'jadwal_2',
pompaAir: true,
pompaPupuk: true,
pots: [1, 2, 3, 4, 5], // Semua pot
durasiDetik: durasi2,
);
}
await NotificationService.instance.scheduleAllWateringExecutions();
debugPrint(
'✅ All watering executions scheduled via notification service',
);
} catch (e) {
debugPrint('❌ Error checking scheduled watering: $e');
debugPrint('❌ Error scheduling watering executions: $e');
}
}
/// Check apakah jadwal harus dijalankan
bool _shouldStartWatering(
String scheduleId,
String currentTime,
String targetTime,
) {
// Cek apakah waktu sekarang sama dengan target
if (currentTime != targetTime) return false;
// Cek apakah sudah berjalan
if (_isWateringActive[scheduleId] == true) return false;
// Cek apakah sudah pernah dijalankan dalam 1 menit terakhir (prevent double trigger)
final lastTime = _lastWateringTime[scheduleId];
if (lastTime != null) {
final diff = DateTime.now().difference(lastTime);
if (diff.inSeconds < 60) return false;
/// Cancel all watering schedule executions
Future<void> _cancelWateringSchedules() async {
try {
await NotificationService.instance.cancelAllWateringSchedules();
debugPrint('✅ All watering schedule executions cancelled');
} catch (e) {
debugPrint('❌ Error cancelling watering schedules: $e');
}
}
return true;
/// Handle watering execution when scheduled alarm fires
/// This is called from NotificationService when notification triggers
Future<void> _handleWateringScheduleExecution(dynamic scheduleData) async {
try {
if (scheduleData is! Map) {
debugPrint('⚠️ Invalid schedule data: $scheduleData');
return;
}
final data = Map<String, dynamic>.from(scheduleData);
final scheduleId = (data['scheduleId'] ?? 'unknown') as String;
final pompaAir = data['pompa_air'] ?? true;
final pompaPupuk = data['pompa_pupuk'] ?? false;
final durasiDetik = data['durasi'] ?? 60;
final potAktif = List<int>.from(data['pot_aktif'] ?? []);
if (potAktif.isNotEmpty) {
debugPrint(
'💧 Executing scheduled watering: $scheduleId (${durasiDetik}s)',
);
await _executeWatering(
scheduleId: scheduleId,
pompaAir: pompaAir,
pompaPupuk: pompaPupuk,
pots: potAktif,
durasiDetik: durasiDetik,
);
} else {
debugPrint('⚠️ No pots selected for $scheduleId');
}
} catch (e) {
debugPrint('❌ Error handling watering execution: $e');
}
}
/// Execute penyiraman dengan durasi tertentu
@ -171,6 +165,26 @@ class KontrolAutomationService {
await _dbService.setMultipleAktuator(offUpdates);
debugPrint('✅ Watering completed for $scheduleId');
// Send notification after watering is done
try {
final potText =
pots.length == 5 ? 'semua pot' : 'pot ${pots.join(', ')}';
await NotificationService.instance.showWateringNotification(
title: 'IrriGo - Penyiraman Selesai',
body:
'Penyiraman selesai untuk $potText selama ${durasiDetik} detik.',
payload: {
'type': 'watering_completed',
'scheduleId': scheduleId,
'pots': pots,
'duration': durasiDetik,
},
);
} catch (notifError) {
debugPrint('⚠️ Error sending notification: $notifError');
}
_isWateringActive[scheduleId] = false;
} catch (e) {
debugPrint('❌ Error executing watering: $e');
@ -182,6 +196,10 @@ class KontrolAutomationService {
/// Start monitoring sensor mode
void startSensorMode() {
// Disabled on mobile app to let Railway worker handle all automation
debugPrint('🌡️ Sensor Mode: Disabled on mobile app (handled by Railway worker instead)');
if (DateTime.now().year > 2000) return; // Early exit to prevent background sensor check in app
if (_isSensorModeActive) return;
// Start connection monitor
@ -390,6 +408,24 @@ class KontrolAutomationService {
'✅ Sensor Mode: Watering completed for POT $potNumber (${elapsedSeconds}s, mode: $mode)',
);
// Send notification after watering is done
try {
await NotificationService.instance.showWateringNotification(
title: 'IrriGo - Penyiraman Sensor Selesai',
body:
'Penyiraman POT $potNumber selesai (kelembapan: $soilValue%, durasi: ${elapsedSeconds}s, mode: $mode).',
payload: {
'type': 'sensor_watering_completed',
'pot': potNumber,
'soil': soilValue,
'duration': elapsedSeconds,
'mode': mode,
},
);
} catch (notifError) {
debugPrint('⚠️ Error sending notification: $notifError');
}
// Pastikan flag ter-reset dengan benar
await Future.delayed(const Duration(milliseconds: 500));
_isWateringActive[potKey] = false;

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import 'package:firebase_database/firebase_database.dart';
import '../models/threshold_model.dart';
import 'notification_service.dart';
class ThresholdService {
static const String kontrolPath = 'kontrol_1';
@ -232,6 +233,10 @@ class ThresholdService {
try {
await _dbRef.child(kontrolPath).child('otomatis').set(enabled);
print('✅ Sensor mode set to ${enabled ? "enabled" : "disabled"}');
await NotificationService.instance.showModeChangedNotification(
modeLabel: 'otomatis berbasis sensor',
enabled: enabled,
);
return true;
} catch (e) {
print('❌ Error setting sensor mode: $e');

View File

@ -8,6 +8,8 @@ import Foundation
import firebase_auth
import firebase_core
import firebase_database
import firebase_messaging
import flutter_local_notifications
import google_sign_in_ios
import shared_preferences_foundation
@ -15,6 +17,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.7"
version: "4.0.9"
args:
dependency: transitive
description:
@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@ -81,14 +81,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
@ -97,22 +89,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dbus:
dependency: transitive
description:
name: dbus
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
url: "https://pub.dev"
source: hosted
version: "0.7.12"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.2.0"
file:
dependency: transitive
description:
@ -157,10 +165,10 @@ packages:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
url: "https://pub.dev"
source: hosted
version: "6.0.2"
version: "6.0.3"
firebase_core_web:
dependency: transitive
description:
@ -193,6 +201,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.6+16"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
url: "https://pub.dev"
source: hosted
version: "15.2.10"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
url: "https://pub.dev"
source: hosted
version: "4.6.10"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
url: "https://pub.dev"
source: hosted
version: "3.10.10"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
url: "https://pub.dev"
source: hosted
version: "0.69.2"
flutter:
dependency: "direct main"
description: flutter
@ -214,6 +254,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35"
url: "https://pub.dev"
source: hosted
version: "17.2.4"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
url: "https://pub.dev"
source: hosted
version: "4.0.1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
url: "https://pub.dev"
source: hosted
version: "7.2.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -292,10 +356,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.7.2"
version: "4.8.0"
json_annotation:
dependency: transitive
description:
@ -308,26 +372,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@ -340,34 +404,34 @@ packages:
dependency: transitive
description:
name: logger
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c"
url: "https://pub.dev"
source: hosted
version: "2.6.2"
version: "2.7.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.18.0"
path:
dependency: transitive
description:
@ -428,10 +492,10 @@ packages:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
version: "6.5.0"
rename:
dependency: "direct main"
description:
@ -545,10 +609,18 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.11"
timezone:
dependency: "direct main"
description:
name: timezone
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
typed_data:
dependency: transitive
description:
@ -558,13 +630,13 @@ packages:
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
dependency: "direct main"
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
@ -606,5 +678,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.2 <4.0.0"
dart: ">=3.10.0-0 <4.0.0"
flutter: ">=3.29.0"

View File

@ -40,6 +40,11 @@ dependencies:
firebase_database: ^11.1.8
google_sign_in: ^6.2.1
shared_preferences: ^2.2.2
fl_chart: ^0.69.0
firebase_messaging: ^15.1.6
flutter_local_notifications: ^17.2.3
timezone: ^0.9.4
vector_math: ^2.1.4
dev_dependencies:
flutter_test:
@ -74,10 +79,10 @@ flutter:
flutter_launcher_icons:
android: true
ios: false
image_path: "assets/images/logo_apsgo.png"
image_path: "assets/images/logo_irrigo.png"
# Uncomment jika ingin adaptive icon (Android 8.0+)
# adaptive_icon_background: "#4CAF50"
# adaptive_icon_foreground: "assets/images/logo_apsgo.png"
# adaptive_icon_foreground: "assets/images/logo_irrigo.png"
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images

BIN
railway-worker.rar Normal file

Binary file not shown.

13
railway-worker/.env Normal file
View File

@ -0,0 +1,13 @@
# Firebase Configuration
# Generated from Service Account JSON on 2026-02-11T00:11:59.245Z
FIREBASE_PROJECT_ID=project-ta-951b4
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-fbsvc@project-ta-951b4.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDiJXKVExyqW7iW\n1JiNP6qkMD25rjxLqA+/b9s1hnZCCuMkt51g85+52VvKDi55JgsnrEZuACB2bi+A\nPc7L9Gw+VHmqJQa4udngojvwN4gll2uYbgJ1DGd6gLrFGsXK3BgWx5/7jZU9YY4p\n2RiQqHSm60Jrn7f2tOen9ImBZveAmsZrGE/3+hkEdGZN/aL94ff+L1bqNLDb7mAP\nKErBjB/5uSjAYHQ8y/lMwQBLBrwhL+PGZtMqjlLTWLzO6LtA4RqLfncptI3lUFu5\nkIOztPM42WzHfvBSao02s2KKV8d/4HzUCY5hq89Wfx3fLzbjvDw/vkr/cwwcIexf\nNjj71eITAgMBAAECggEAPeV0/qNsAZ64H6RRmrt1x5mik1tCioyVvtZtP7GtVXco\nY2jULPCRY6s/ycZm5ubIP9QRtNLZD8EpxCZmnwEnzUmOwUiAFfhcT3ToBeAVJgDT\nRLW7e3DpM6jfNcqVn2ftOfLqcU33roSwhfizFgjdYMBcfhuJnP83tosiRaY6kNqI\nNWkMvpBNVvz2aJQYECracQzKGKlgh17GRzpMs6gw1DTMK5L4E1gCnsXsshqMv2jO\nXTdlSvtkY9/WPYxygVzApiKgxIQxE0N0eJhtqxJ1LIK5l6iGC4RI0J66TTzxWda9\nAQo/p4V2x/1FIJv2xbchQGzwijf0WcAq7SLY0VuffQKBgQD4BlCbrIZcPG/5RHfV\nXYybJcU7oyHVskJ+Y9amoCIzyNEwdK/RDWuAdmGt0IOXpTY3gwideefToWmL3BmD\nOZxz3DW9L3+vzOlTtI802ZFN0iCBtfljjtYCjMPdCPnz195RkMSlKtJqouvBocmL\n5ElaEvtsjoRtjcaLv+JZfl/QPwKBgQDpawjaTIHunT2OrNlOLPRXcePOmmvUrguB\n59KwAyxj2q5vXBuRDF3wdZoYA4oLh9lAKxwCjIL4FmTJ/IL2x9G94pR9VaOkzx3Q\nUr6yp5Y5OpEghmm7NCpj/Ri/JHQpLQb86xgzrjd2IpaBbTpKY769C8iV0vcLvUhD\n1siqsE/5LQKBgD9H1S8y4IEOBn+xwWVV3fcDtwPVRl+wgJigvnZ66t8NgJn9W5JZ\n+zCmg5uGVNzPETsumncbUFPFnhlKCdRaUZmZ+LgRM2HWRT3PFq6kSBdz4klV9ZA6\nNE4oPhzaGnPlgtqKtjFx8Ie2k7Iupi6kCzcJGs8CubT99EdZqNWukaGfAoGBAMWH\n++zpVAe/b4mfQyLCLmEWE4gS7HAGfdyId6bWeKlkNbwtwXJledX1X9s0m40YgMSp\n9sE9cdRK9y5sD9SR0zCTX8AVSjA9ymgyrgj4g8uYgZp5xV5UTg9h76BKVDdIUKKW\nvVt28p55tM6AxhSQBqlrIGuSJdj8bPxj9ltka7ldAoGBAN4Uzy6njLEDwvt4bK+m\npUwL9vOeH182SZ/gM26gh/4ov5qAdoyRa78Ek1EZMiTJ3xIJdckNtuNJB8/bIcdH\nh2DriUnxEOoU5gu6ErVIKcfVn/7JTvBfnkE+lVLnCed4IBr5Kh4Woaa74s5r7JMp\ndy24IcgxQ5sczYS5yrXeUqAs\n-----END PRIVATE KEY-----\n"
FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com
# Redis Configuration (Railway will auto-provide these)
# REDIS_HOST=localhost
# REDIS_PORT=6379
# REDIS_PASSWORD=

View File

@ -1,27 +1,45 @@
# Dependencies
node_modules/
package-lock.json
yarn.lock
# Environment variables
.env
.env.local
# Logs
logs/
# Miscellaneous
*.class
*.log
npm-debug.log*
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.pyc
*.swp
*.swo
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# Firebase service account
serviceAccount.json
firebase-key.json
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
railway-worker/.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: android
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: ios
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: linux
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: macos
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: web
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: windows
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

5
railway-worker/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"chat.tools.terminal.autoApprove": {
"git init": true
}
}

View File

@ -1,148 +1,62 @@
# ApsGo Railway Worker
# ApsGo - Sistem Penjadwalan Penyiraman Otomatis
Background worker service untuk sistem otomasi IoT ApsGo. Service ini berjalan 24/7 di cloud untuk menjalankan penjadwalan dan automation bahkan ketika aplikasi mobile ditutup atau handphone pengguna mati.
Aplikasi Flutter untuk kontrol otomatis sistem penyiraman tanaman berbasis IoT dengan Firebase Realtime Database dan Railway Worker.
## Features
## Status Terkini
- ✅ **Waktu Mode**: Penjadwalan berdasarkan waktu (cron-based)
- ✅ **Sensor Mode**: Otomasi berdasarkan threshold kelembapan tanah
- ✅ **Auto History Logging**: Record data sensor setiap 30 menit
- ✅ **Redis Queue**: Prevent race conditions dan manage concurrent tasks
- ✅ **Graceful Shutdown**: Clean shutdown dengan safety turn-off semua aktuator
- ✅ **Health Monitoring**: Auto health check setiap 5 menit
- ✅ **Auto Cleanup**: Hapus history lama otomatis (retain 10 hari)
✅ **Sistem berjalan real-time dengan delay 20 detik/section**
⚠️ **Catatan:** 1 waktu penjadwalan digunakan untuk banyak pompa sekaligus
## Tech Stack
## Komponen Sistem
- **Node.js**: Runtime environment
- **Firebase Admin SDK**: Realtime Database integration
- **BullMQ**: Robust job queue dengan Redis
- **Redis**: In-memory database untuk queue dan caching
- **Cron**: Scheduled tasks
### 1. Flutter Mobile App
- Kontrol manual aktuator (pompa & mosvet)
- Penjadwalan otomatis (waktu mode)
- Monitoring sensor real-time
- History logging
## Setup Local Development
### 2. Railway Worker (Node.js)
- Background automation 24/7
- Scheduler dengan timezone Asia/Jakarta
- Firebase SDK dengan REST API fallback
- BullMQ job queue untuk reliability
- Auto-recovery dari timeout issues
1. Install dependencies:
## Fitur Utama
- **Dual Mode**: Kontrol manual + penjadwalan otomatis
- **Real-time Sync**: Firebase Realtime Database
- **Reliable Execution**: Timeout handling + REST API fallback
- **History Tracking**: Log semua aktivitas penyiraman
- **Safety Mechanisms**: Auto-shutoff jika error
## Teknologi
- **Frontend**: Flutter (Dart)
- **Backend Worker**: Node.js v18+
- **Database**: Firebase Realtime Database
- **Queue**: Redis + BullMQ
- **Deployment**: Railway.app
- **Authentication**: Firebase Auth + Google Sign-In
## Getting Started
### Mobile App
```bash
flutter pub get
flutter run
```
### Railway Worker
```bash
cd railway-worker
npm install
node worker.js
```
2. Copy `.env.example` ke `.env` dan isi dengan credentials Firebase Anda:
```bash
cp .env.example .env
```
## Dokumentasi
3. Setup Redis lokal (gunakan Docker):
```bash
docker run -d -p 6379:6379 redis:latest
```
4. Run worker:
```bash
npm run dev # Development mode dengan nodemon
# atau
npm start # Production mode
```
## Deploy to Railway
Lihat file `DEPLOYMENT_GUIDE.md` untuk step-by-step deployment ke Railway.
## Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `FIREBASE_PROJECT_ID` | Firebase project ID | ✅ |
| `FIREBASE_CLIENT_EMAIL` | Firebase service account email | ✅ |
| `FIREBASE_PRIVATE_KEY` | Firebase service account private key | ✅ |
| `FIREBASE_DATABASE_URL` | Firebase Realtime Database URL | ✅ |
| `REDIS_HOST` | Redis hostname | ✅ |
| `REDIS_PORT` | Redis port (default: 6379) | ❌ |
| `REDIS_PASSWORD` | Redis password (if required) | ❌ |
## Architecture
```
Flutter App (Mobile)
Firebase Realtime DB ← ESP32/Hardware
Railway Worker (This service)
Redis Queue
```
## How It Works
### Waktu Mode
- Worker check Firebase `/kontrol` setiap 30 detik
- Jika `waktu_1` atau `waktu_2` match dengan waktu sekarang, add job ke queue
- Job akan diprocess oleh worker untuk nyalakan pompa dan valve
- Setelah durasi selesai, otomatis matikan
### Sensor Mode
- Worker listen ke Firebase `/data` secara realtime
- Jika `soil_X` < `batas_bawah`, trigger watering untuk pot tersebut
- Ada cooldown 2 menit per pot untuk prevent over-watering
- Support 2 mode: `fixed` (durasi tetap) dan `smart` (sampai mencapai batas_atas)
### Safety Features
- Concurrency: 1 (hanya 1 job diprocess pada satu waktu)
- Debouncing: Minimum 2 menit antar penyiraman per pot
- Error handling: Jika error, otomatis turn OFF semua aktuator
- Graceful shutdown: Clean up resources saat restart/shutdown
## Monitoring
Worker akan log semua aktivitas ke console:
- ✅ Success operations
- ❌ Errors dengan details
- 💧 Watering jobs progress
- 📊 History logging
- 💚 Health check status
Di Railway dashboard, Anda bisa:
- View logs realtime
- Monitor CPU/Memory usage
- Setup alerts untuk failures
## Maintenance
### Manual Queue Management
Untuk clear queue (jika ada masalah):
```javascript
const { Queue } = require('bullmq');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const queue = new Queue('watering', { connection: redis });
// Clear all jobs
await queue.obliterate();
```
### Database Cleanup
History otomatis di-cleanup setiap hari jam 2 pagi, hanya retain 10 hari terakhir.
## Troubleshooting
### Worker tidak berjalan
1. Check environment variables
2. Check Firebase credentials
3. Check Redis connection
### Job tidak diprocess
1. Check queue status di logs
2. Verify Firebase rules mengizinkan admin access
3. Check concurrency setting
### Memory leak
- Worker menggunakan BullMQ yang sudah optimize untuk long-running process
- Auto cleanup completed jobs (retain last 100)
- Auto cleanup failed jobs (retain last 50)
## License
MIT
- [Railway Setup Guide](RAILWAY_QUICK_START.md)
- [Deployment Guide](DEPLOYMENT_GUIDE.md)
- [Debug & Testing Guide](DEBUG_TESTING_GUIDE.md)
- [Troubleshooting](RAILWAY_TROUBLESHOOTING.md)

View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
railway-worker/android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,45 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
namespace = "com.kampus.apsgo"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.kampus.apsgo"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "217854138058",
"project_id": "project-ta-951b4",
"storage_bucket": "project-ta-951b4.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:217854138058:android:b8bd92665b1766ad0c4633",
"android_client_info": {
"package_name": "com.kampus.apsgo"
}
},
"oauth_client": [
{
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCrJAAUZAPettM5AwPbV3rAGlT4Jx922m8"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="ApsGo"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.project_ta
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,31 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.google.gms:google-services:4.4.2")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

View File

@ -0,0 +1,28 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

1898
railway-worker/cekworker.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,160 @@
/**
* Script untuk membersihkan semua history data Firebase
* dan mengisinya dengan dummy data (40-80) untuk testing
*
* Jalankan dengan: node clear-and-populate-dummy-data.js
*/
const admin = require('firebase-admin');
const path = require('path');
// Initialize Firebase Admin
const serviceAccountPath = path.join(__dirname, 'service-account-key.json');
// Cek apakah file key ada
try {
const serviceAccount = require(serviceAccountPath);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.FIREBASE_DATABASE_URL || 'https://your-project.firebaseio.com'
});
} catch (error) {
console.error('❌ Service account key tidak ditemukan!');
console.error(' Pastikan file service-account-key.json ada di railway-worker/');
process.exit(1);
}
const db = admin.database();
// Fungsi untuk generate dummy data
function generateDummyData(startDate, endDate) {
const data = {};
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
const dateKey = formatDate(currentDate);
const dateData = {};
// Generate data setiap 30 menit
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
const timeKey = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
// Generate random values 40-80
const soil1 = 40 + Math.random() * 40;
const soil2 = 40 + Math.random() * 40;
const soil3 = 40 + Math.random() * 40;
const soil4 = 40 + Math.random() * 40;
const soil5 = 40 + Math.random() * 40;
dateData[timeKey] = {
soil_1: soil1.toFixed(1),
soil_2: soil2.toFixed(1),
soil_3: soil3.toFixed(1),
soil_4: soil4.toFixed(1),
soil_5: soil5.toFixed(1),
suhu: (20 + Math.random() * 15).toFixed(1), // 20-35°C
kelembapan: (40 + Math.random() * 40).toFixed(1), // 40-80%
ldr: Math.floor(300 + Math.random() * 700).toString(), // 300-1000
source: 'dummy-data',
timestamp: new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate(),
hour,
minute
).getTime().toString(),
type: 'sensor_reading'
};
}
}
data[dateKey] = dateData;
currentDate.setDate(currentDate.getDate() + 1);
}
return data;
}
// Fungsi format date
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// Main function
async function clearAndPopulateDummyData() {
try {
console.log('\n🔄 Mulai proses clear dan populate dummy data...\n');
// Step 1: Clear existing history data
console.log('⏳ Step 1: Menghapus data history yang sudah ada...');
const historyRef = db.ref('history');
const snapshot = await historyRef.once('value');
if (snapshot.exists()) {
await historyRef.remove();
console.log('✅ Data history berhasil dihapus!\n');
} else {
console.log(' Tidak ada data history untuk dihapus\n');
}
// Step 2: Generate dummy data untuk 7 hari terakhir
console.log('⏳ Step 2: Generate dummy data untuk 7 hari terakhir...');
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
const dummyData = generateDummyData(startDate, endDate);
const totalEntries = Object.values(dummyData).reduce(
(sum, dateData) => sum + Object.keys(dateData).length,
0
);
console.log(`✅ Generated ${totalEntries} data points untuk ${Object.keys(dummyData).length} hari\n`);
// Step 3: Upload dummy data ke Firebase
console.log('⏳ Step 3: Upload dummy data ke Firebase...');
await historyRef.set(dummyData);
console.log('✅ Dummy data berhasil diupload!\n');
// Step 4: Verify data
console.log('⏳ Step 4: Verifikasi data...');
const verifySnapshot = await historyRef.once('value');
const uploadedData = verifySnapshot.val();
const uploadedEntries = Object.values(uploadedData).reduce(
(sum, dateData) => sum + Object.keys(dateData).length,
0
);
console.log(`✅ Verifikasi: ${uploadedEntries} data points berhasil tersimpan\n`);
// Summary
console.log('═══════════════════════════════════════════════════');
console.log('✨ SUMMARY');
console.log('═══════════════════════════════════════════════════');
console.log(`📅 Periode: ${formatDate(startDate)} hingga ${formatDate(endDate)}`);
console.log(`📊 Total data points: ${uploadedEntries}`);
console.log(`📈 Nilai range: 40-80`);
console.log(`🔄 Status: BERHASIL\n`);
console.log('🎯 Instruksi selanjutnya:');
console.log('1. Buka aplikasi di Flutter');
console.log('2. Buka halaman Histori');
console.log('3. Data dummy akan dimuat otomatis dari Firebase');
console.log('4. Grafik harus terlihat clean tanpa outliers\n');
process.exit(0);
} catch (error) {
console.error('\n❌ ERROR:', error.message);
console.error('\nDetail:', error);
process.exit(1);
}
}
// Run the script
clearAndPopulateDummyData();

View File

@ -0,0 +1,110 @@
{
"aktuator": {
"mosvet_1": false,
"mosvet_2": false,
"mosvet_3": false,
"mosvet_4": false,
"mosvet_5": false,
"mosvet_6": false,
"mosvet_7": false,
"mosvet_8": false
},
"data": {
"pot_1": {
"kelembaban": 50,
"suhu": 28,
"timestamp": 1708070400000
},
"pot_2": {
"kelembaban": 45,
"suhu": 27,
"timestamp": 1708070400000
},
"pot_3": {
"kelembaban": 52,
"suhu": 29,
"timestamp": 1708070400000
},
"pot_4": {
"kelembaban": 48,
"suhu": 28,
"timestamp": 1708070400000
},
"pot_5": {
"kelembaban": 51,
"suhu": 27,
"timestamp": 1708070400000
}
},
"history": {
"2026-02-16": {
"log_1": {
"timestamp": 1708070400000,
"type": "waktu_jadwal_1",
"pots": [1, 2, 3],
"durasi": 60,
"pompa_air": true,
"pompa_pupuk": false
}
}
},
"kontrol": {
"waktu": true,
"sensor": false,
"otomatis": true,
"batas_atas": 32,
"batas_bawah": 11,
"mode_sensor": "smart",
"durasi_sensor": 600,
"jadwal_1": {
"aktif": true,
"waktu": "08:00",
"durasi": 60,
"pot_aktif": [1, 2, 3],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_2": {
"aktif": true,
"waktu": "09:00",
"durasi": 45,
"pot_aktif": [4, 5],
"pompa_air": true,
"pompa_pupuk": true
},
"jadwal_3": {
"aktif": true,
"waktu": "16:00",
"durasi": 30,
"pot_aktif": [1, 2, 3, 4, 5],
"pompa_air": true,
"pompa_pupuk": true
},
"jadwal_4": {
"aktif": false,
"waktu": "12:00",
"durasi": 40,
"pot_aktif": [2, 4],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_5": {
"aktif": false,
"waktu": "18:00",
"durasi": 50,
"pot_aktif": [1, 5],
"pompa_air": true,
"pompa_pupuk": false
}
}
}

View File

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"project-ta-951b4","appId":"1:217854138058:android:b8bd92665b1766ad0c4633","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"project-ta-951b4","configurations":{"android":"1:217854138058:android:b8bd92665b1766ad0c4633","ios":"1:217854138058:ios:aeee768efda8c5d80c4633","macos":"1:217854138058:ios:d9fb00752ef371fe0c4633","web":"1:217854138058:web:50a5bcd5a61ac1820c4633","windows":"1:217854138058:web:233d93e01272a0870c4633"}}}}}}

34
railway-worker/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

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