Merge production/main into main

This commit is contained in:
Wizznu 2026-06-02 07:51:56 +07:00
commit b81cc38773
21 changed files with 4056 additions and 0 deletions

54
.dockerignore Normal file
View File

@ -0,0 +1,54 @@
# ===================================================================
# .dockerignore - File yang diabaikan saat build Docker image
# Mengurangi ukuran image dan mempercepat build
# ===================================================================
# Dependencies (akan di-install ulang di Dockerfile)
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files (Railway akan inject environment variables)
.env
.env.local
.env.*.local
.env.example
# Git files
.git/
.gitignore
.github/
# Documentation
README.md
*.md
docs/
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Test files
test/
*.test.js
coverage/
# Build artifacts
dist/
build/
# Logs
logs/
*.log
# Temporary files
tmp/
temp/

13
.env.example Normal file
View File

@ -0,0 +1,13 @@
# Firebase Configuration
FIREBASE_PROJECT_ID=your-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@your-project-id.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYourPrivateKeyHere\n-----END PRIVATE KEY-----\n"
FIREBASE_DATABASE_URL=https://your-project-id-default-rtdb.firebaseio.com
# Redis Configuration (Railway will auto-provide this)
REDIS_HOST=redis.railway.internal
REDIS_PORT=6379
REDIS_PASSWORD=
# Or use REDIS_URL (Railway format)
# REDIS_URL=redis://default:password@redis.railway.internal:6379

27
.env.template Normal file
View File

@ -0,0 +1,27 @@
# Railway Worker Environment Variables
# Copy this file to .env and fill with your Firebase credentials
# 🔥 Firebase Configuration
# Get these from Firebase Console → Project Settings → Service Accounts → Generate Private Key
FIREBASE_PROJECT_ID=project-ta-951b4
FIREBASE_CLIENT_EMAIL=
FIREBASE_PRIVATE_KEY=
FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com
# 📝 Instructions:
# 1. FIREBASE_CLIENT_EMAIL:
# Format: firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com
#
# 2. FIREBASE_PRIVATE_KEY:
# - Must be wrapped in quotes: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
# - Keep the \n characters (they are important!)
# - Copy ENTIRE private_key value from downloaded JSON
#
# Example:
# FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEF...\n-----END PRIVATE KEY-----\n"
# ⚠️ SECURITY WARNING:
# - NEVER commit .env file to git
# - NEVER share these credentials
# - This file is already in .gitignore

241
DEBUGGING_LOGS.md Normal file
View File

@ -0,0 +1,241 @@
# 🐛 Debugging Guide - Railway Worker
## Masalah yang Ditemukan
### 1⃣ **Log Scheduler Tidak Muncul**
**Penyebab:**
- Kondisi logging `now.getSeconds() < 60` tidak efektif
- Interval check 60 detik tidak sinkron dengan menit yang tepat
- Tidak ada konfirmasi bahwa fungsi `checkScheduledWatering()` berjalan
**Solusi:**
✅ Tambah counter untuk track setiap check
✅ Log setiap kali fungsi dipanggil: `⏱️ CHECK #X`
✅ Log detail setiap 3 check atau setiap 5 menit
✅ Jalankan check pertama kali setelah 5 detik (tidak tunggu 60 detik)
---
### 2⃣ **Aktuator Tidak Trigger**
**Kemungkinan Penyebab:**
- Node `/aktuator` tidak ada di Firebase
- Format waktu tidak match dengan jadwal
- Queue tidak menjalankan job
- Firebase update gagal tanpa error log
**Solusi:**
✅ Validasi node aktuator saat startup
✅ Log verbose untuk setiap Firebase update
✅ Verify state aktuator setelah update
✅ Log queue status (active/waiting jobs)
---
## 📊 Log Baru yang Akan Muncul
### Saat Startup (5 detik pertama):
```
🔧 RUNNING DIAGNOSTIC CHECKS...
🕐 CURRENT TIME ANALYSIS:
Server Local: Wed Feb 11 2026 10:20:05 GMT+0700
Asia/Jakarta: 11/2/2026, 10.20.05
TZ Env: Asia/Jakarta
HH:MM Format: 10:20
📋 FIREBASE KONTROL:
Mode Waktu: ENABLED ✅
Waktu 1: 10:18
Waktu 2: 09:00
🔍 CHECKING AKTUATOR NODE...
✅ Aktuator node exists:
mosvet_1: false
mosvet_2: false
...
🚀 Running first schedule check immediately...
```
### Setiap 60 Detik:
```
⏱️ CHECK #1: 10:20:15 | Mode: ✅
⏱️ CHECK #2: 10:21:15 | Mode: ✅
⏱️ CHECK #3: 10:22:15 | Mode: ✅
📅 Date: 2026-02-11
🕐 Current: 10:22 (11/2/2026, 10.22.15)
Mode Waktu: ✅ ENABLED
Jadwal 1: 10:18
Jadwal 2: 09:00
```
### Saat Jadwal Match:
```
⏱️ CHECK #8: 10:25:00 | Mode: ✅
📅 Date: 2026-02-11
🕐 Current: 10:25 (11/2/2026, 10.25.00)
Mode Waktu: ✅ ENABLED
Jadwal 1: 10:25 🔔 MATCH!
Jadwal 2: 09:00
🕐 JADWAL 1 TRIGGERED: 10:25
🎯 Attempting to add job to queue...
✅ Successfully added to queue: jadwal_1_2026-02-11_10:25
📊 Queue status: 1 active, 0 waiting
💧 Processing Job: jadwal_1_2026-02-11_10:25
Type: waktu_jadwal_1
Pots: [1, 2, 3, 4, 5]
Duration: 300s
🔛 Turning ON: mosvet_1, mosvet_2, mosvet_3, mosvet_4, mosvet_5, mosvet_6, mosvet_7
📌 Firebase path: aktuator
📝 Updates: {
"mosvet_1": true,
"mosvet_2": true,
...
}
✅ Firebase update successful
🔍 Verified aktuator state: { ... }
⏳ 300s remaining...
⏳ 290s remaining...
...
```
### Saat Sensor Trigger:
```
🌡️ SENSOR CHECK #10 | Mode Otomatis: ✅
🌡️ SENSOR TRIGGERED: POT 1
Soil moisture: 35% < 40%
Mode: smart, Duration: 600s
📌 Added to queue: sensor-pot-1-...
```
---
## 🔧 Cara Testing
### Test 1: Verifikasi Worker Berjalan
1. Check Railway logs untuk `CHECK #X` setiap menit
2. Pastikan counter naik: `CHECK #1, #2, #3...`
3. Jika tidak ada, worker crash atau interval tidak jalan
### Test 2: Test Jadwal Manual
1. Lihat log `CURRENT TIME ANALYSIS` untuk waktu saat ini
2. Set `waktu_1` di Firebase = waktu saat ini + 2 menit
- Contoh: Sekarang 10:30 → set `waktu_1: "10:32"`
3. Tunggu 2 menit
4. Harus muncul log `JADWAL 1 TRIGGERED: 10:32`
### Test 3: Verifikasi Queue
1. Saat jadwal trigger, cek log `Queue status:`
2. Pastikan ada `1 active` atau `1 waiting`
3. Kemudian lihat `Processing Job:` muncul
### Test 4: Verifikasi Firebase Aktuator
1. Saat job running, buka Firebase Console
2. Lihat node `/aktuator`
3. Pastikan mosvet yang relevan = `true`
4. Setelah durasi selesai, harus kembali `false`
---
## 🚨 Troubleshooting
### Problem: Log CHECK tidak muncul
**Diagnosis:** Worker crash atau interval tidak jalan
**Solution:**
- Check Railway logs untuk error
- Restart worker di Railway Dashboard
### Problem: CHECK muncul tapi tidak TRIGGERED
**Diagnosis:** Format waktu tidak match
**Solution:**
- Cek log detail (setiap 3 check): lihat "Current: HH:MM"
- Bandingkan dengan "Jadwal 1: HH:MM" di Firebase
- Pastikan format sama persis (2 digit: "09:00" bukan "9:00")
### Problem: TRIGGERED tapi tidak Processing
**Diagnosis:** Queue error atau Redis disconnect
**Solution:**
- Cek log `HEALTH CHECK` untuk Redis status
- Cek log `Successfully added to queue`
- Restart Redis service jika perlu
### Problem: Processing tapi aktuator tidak ON
**Diagnosis:** Firebase update gagal atau node tidak ada
**Solution:**
- Cek log `Firebase update successful`
- Cek log `Verified aktuator state`
- Lihat Firebase Console untuk konfirmasi nilai
---
## 📝 Checklist Debugging
- [ ] Worker menghasilkan log `CHECK #X` setiap menit
- [ ] Counter naik terus (tidak reset tanpa alasan)
- [ ] Mode Waktu menunjukkan ✅ ENABLED di Firebase
- [ ] Format waktu di log match dengan jadwal (HH:MM)
- [ ] Saat match, muncul "🔔 MATCH!"
- [ ] Muncul "JADWAL X TRIGGERED"
- [ ] Muncul "Successfully added to queue"
- [ ] Queue status menunjukkan job active/waiting
- [ ] Muncul "Processing Job:"
- [ ] Muncul "Firebase update successful"
- [ ] Muncul "Verified aktuator state"
- [ ] Node `/aktuator` di Firebase berubah nilai
- [ ] Setelah durasi, aktuator OFF dan muncul log history
---
## 🎯 Expected Behavior
**Normal Flow:**
1. Worker start → Diagnostic checks (5 detik)
2. First check immediately → CHECK #1
3. Check setiap 60 detik → CHECK #2, #3, #4...
4. Saat match → TRIGGERED
5. Add to queue → Queue status updated
6. Worker process job → Aktuator ON
7. Wait duration → Progress logs setiap 10s
8. Duration done → Aktuator OFF
9. Log history → Complete
**Timeline Example:**
```
10:20:05 - Worker start + diagnostics
10:20:10 - First check (CHECK #1)
10:21:10 - CHECK #2
10:22:10 - CHECK #3 (detail log)
10:23:10 - CHECK #4
10:24:10 - CHECK #5
10:25:10 - CHECK #6 (detail log) + JADWAL TRIGGERED!
10:25:15 - Job processing started
10:25:15 - Aktuator ON
10:30:15 - Aktuator OFF (after 300s)
10:30:15 - History logged
10:30:15 - Job completed
```
---
## 💡 Tips
1. **Monitor Railway logs secara real-time** saat testing
2. **Gunakan waktu + 2 menit** untuk test (beri waktu buffer)
3. **Cek Firebase Console** bersamaan dengan Railway logs
4. **Screenshot error logs** jika ada masalah untuk analisis
5. **Restart worker** setelah update kode atau environment variable
---
## 📞 Support
Jika masih ada masalah setelah ikuti guide ini:
1. Export Railway logs (10 menit terakhir)
2. Screenshot Firebase `/kontrol` node
3. Screenshot Firebase `/aktuator` node
4. Catat waktu test dilakukan
5. Dokumentasikan expected vs actual behavior

45
Dockerfile Normal file
View File

@ -0,0 +1,45 @@
# ===================================================================
# Dockerfile untuk ApsGo Railway Worker
# Background service untuk IoT automation dengan BullMQ + Redis
# ===================================================================
# Base image: Node.js 18 LTS (Alpine untuk size kecil)
FROM node:18-alpine
# Metadata
LABEL maintainer="ApsGo Team"
LABEL description="Railway Worker untuk ApsGo - 24/7 IoT Automation"
LABEL version="1.0.0"
# Set working directory
WORKDIR /app
# Copy package files dulu (untuk leverage Docker cache)
COPY package.json package-lock.json* ./
# Install dependencies
# --production: hanya production dependencies (tanpa devDependencies)
# --silent: suppress npm warnings
RUN npm install --production --silent
# Copy source code
COPY worker.js ./
# Environment variables (akan di-override oleh Railway)
ENV NODE_ENV=production
ENV LOG_LEVEL=info
# Health check (optional - Railway bisa monitor via process)
# Cek apakah process masih berjalan setiap 30 detik
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "process.exit(0)"
# Expose port (OPTIONAL - worker tidak butuh port)
# Tapi tetap define untuk dokumentasi
# EXPOSE 3000
# User non-root untuk security (best practice)
USER node
# Start command: jalankan worker
CMD ["node", "worker.js"]

436
FLEXIBLE_SCHEDULE_GUIDE.md Normal file
View File

@ -0,0 +1,436 @@
# 🗓️ Panduan Sistem Penjadwalan Fleksibel
## ✨ Fitur Baru
Sistem penjadwalan sekarang **fully flexible**:
- ✅ **Dynamic schedules**: Tambah jadwal_1, jadwal_2, ... jadwal_N
- ✅ **Per-schedule pot selection**: Setiap jadwal bisa pilih pot mana yang aktif
- ✅ **Per-schedule configuration**: Tiap jadwal punya durasi & pompa sendiri
- ✅ **Enable/Disable**: Bisa matikan jadwal tanpa hapus data
- ✅ **No code change needed**: Tambah/ubah jadwal langsung di Firebase!
## 📋 Struktur Firebase Baru
### Contoh Sesuai Kebutuhan User:
```json
{
"kontrol": {
"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
}
}
}
```
### Penjelasan Field:
| Field | Tipe | Wajib? | Default | Keterangan |
|-------|------|--------|---------|------------|
| `aktif` | boolean | ❌ | `true` | Enable/disable jadwal |
| `waktu` | string | ✅ | - | Format "HH:MM" (24 jam) |
| `durasi` | number | ❌ | `60` | Durasi penyiraman (detik) |
| `pot_aktif` | array | ✅ | - | Array pot yang aktif: `[1, 2, 3]` |
| `pompa_air` | boolean | ❌ | `true` | Nyalakan pompa air |
| `pompa_pupuk` | boolean | ❌ | `false` | Nyalakan pompa pupuk |
## 🚀 Cara Setup di Firebase
### 1. Buka Firebase Console
1. Go to: https://console.firebase.google.com
2. Pilih project **ApsGo**
3. Klik **Realtime Database**
4. Klik **Data** tab
### 2. Setup Struktur Awal
Klik di path `/kontrol` dan edit JSON:
```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
},
"jadwal_3": {
"aktif": true,
"waktu": "16:00",
"durasi": 30,
"pot_aktif": [1, 2, 3, 4, 5],
"pompa_air": true,
"pompa_pupuk": true
}
}
}
```
### 3. Tambah Jadwal Baru
Untuk tambah jadwal baru, tinggal tambah `jadwal_4`, `jadwal_5`, dst:
```json
"jadwal_4": {
"aktif": true,
"waktu": "12:00",
"durasi": 40,
"pot_aktif": [2, 4],
"pompa_air": true,
"pompa_pupuk": false
}
```
**Tidak perlu restart worker!** Worker akan otomatis detect jadwal baru.
### 4. Disable Jadwal Sementara
Ubah `aktif` jadi `false`:
```json
"jadwal_3": {
"aktif": false,
"waktu": "16:00",
...
}
```
Jadwal tidak akan trigger, tapi data tetap tersimpan.
## 📱 Contoh Penggunaan
### Skenario 1: Pagi & Sore
**Kebutuhan:**
- Pagi (08:00): Semua pot disirami
- Sore (16:00): Hanya pot 1, 3, 5
**Setup:**
```json
"jadwal_1": {
"aktif": true,
"waktu": "08:00",
"durasi": 60,
"pot_aktif": [1, 2, 3, 4, 5],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_2": {
"aktif": true,
"waktu": "16:00",
"durasi": 45,
"pot_aktif": [1, 3, 5],
"pompa_air": true,
"pompa_pupuk": true
}
```
### Skenario 2: Per-Pot Berbeda
**Kebutuhan:**
- Pot 1 & 2: Pagi (07:00), Siang (12:00), Sore (17:00)
- Pot 3 & 4: Pagi (08:00), Sore (16:00)
- Pot 5: Hanya pagi (09:00)
**Setup:**
```json
"jadwal_1": {
"aktif": true,
"waktu": "07:00",
"durasi": 60,
"pot_aktif": [1, 2],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_2": {
"aktif": true,
"waktu": "08:00",
"durasi": 60,
"pot_aktif": [3, 4],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_3": {
"aktif": true,
"waktu": "09:00",
"durasi": 60,
"pot_aktif": [5],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_4": {
"aktif": true,
"waktu": "12:00",
"durasi": 45,
"pot_aktif": [1, 2],
"pompa_air": true,
"pompa_pupuk": true
},
"jadwal_5": {
"aktif": true,
"waktu": "16:00",
"durasi": 50,
"pot_aktif": [3, 4],
"pompa_air": true,
"pompa_pupuk": true
},
"jadwal_6": {
"aktif": true,
"waktu": "17:00",
"durasi": 40,
"pot_aktif": [1, 2],
"pompa_air": true,
"pompa_pupuk": false
}
```
### Skenario 3: Testing
**Kebutuhan:** Test pot 3 saja setiap 15 menit
**Setup:**
```json
"jadwal_test_1": {
"aktif": true,
"waktu": "09:00",
"durasi": 10,
"pot_aktif": [3],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_test_2": {
"aktif": true,
"waktu": "09:15",
"durasi": 10,
"pot_aktif": [3],
"pompa_air": true,
"pompa_pupuk": false
},
"jadwal_test_3": {
"aktif": true,
"waktu": "09:30",
"durasi": 10,
"pot_aktif": [3],
"pompa_air": true,
"pompa_pupuk": false
}
```
## 🔍 Monitoring & Logs
### Log yang Normal:
```
⏱️ 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: jadwal_1_2026-02-16_08_00
```
### Log Jika Jadwal Disabled:
```
⏱️ CHECK #15: 08:00:05 | Mode: ✅
📋 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]
```
### Log Error/Warning:
```
⚠️ jadwal_4: Invalid structure, skipping
⚠️ jadwal_5: No active pots defined, skipping
```
## ⚙️ Advanced Configuration
### Limit Maksimal Jadwal
Tidak ada limit! Bisa tambah jadwal_1 sampai jadwal_100 kalau perlu.
**Rekomendasi:**
- **Normal use**: 3-10 jadwal
- **Complex greenhouse**: 10-20 jadwal
- **Maximum tested**: 50 jadwal (still fast!)
### Naming Convention
Worker detect semua key yang mulai dengan `jadwal_`:
- ✅ `jadwal_1`, `jadwal_2`, `jadwal_3`
- ✅ `jadwal_pagi`, `jadwal_sore`, `jadwal_malam`
- ✅ `jadwal_test_1`, `jadwal_test_2`
- ❌ `schedule_1` (tidak akan terdetect)
### Validasi Otomatis
Worker otomatis validasi:
- ✅ `aktif` field (skip jika `false`)
- ✅ `waktu` field (skip jika tidak ada atau tidak match)
- ✅ `pot_aktif` array (skip jika kosong atau invalid)
- ✅ Default values untuk field opsional
## 🔄 Migration dari Format Lama
### Format Lama (Legacy):
```json
{
"kontrol": {
"waktu": true,
"waktu_1": "08:00",
"durasi_1": 60,
"waktu_2": "16:00",
"durasi_2": 45
}
}
```
**Masih supported!** Worker tetap baca `waktu_1` dan `waktu_2`.
### Migrasi Bertahap:
1. **Tetap pakai format lama** sambil test format baru
2. **Tambah jadwal baru** dengan format baru
3. **Disable format lama** setelah yakin
4. **Hapus format lama** setelah beberapa hari
### Contoh Transisi:
```json
{
"kontrol": {
"waktu": true,
// Format lama (masih jalan!)
"waktu_1": "08:00",
"durasi_1": 60,
// Format baru
"jadwal_2": {
"aktif": true,
"waktu": "09:00",
"durasi": 45,
"pot_aktif": [4, 5],
"pompa_air": true,
"pompa_pupuk": true
}
}
}
```
## 🛠️ Troubleshooting
### Jadwal tidak trigger
**Check:**
1. ✅ `kontrol.waktu` = `true`?
2. ✅ `jadwal_X.aktif` = `true`?
3. ✅ `jadwal_X.waktu` format "HH:MM" (2 digit)?
4. ✅ `jadwal_X.pot_aktif` array tidak kosong?
5. ✅ Worker running di Railway?
### Pot salah yang menyala
**Check:**
1. ✅ `pot_aktif` array benar? `[1, 2, 3]` bukan `["1", "2", "3"]`
2. ✅ Tidak ada jadwal lain yang trigger di waktu sama?
3. ✅ Check logs: "Pot aktif: [...]"
### Durasi tidak sesuai
**Check:**
1. ✅ `durasi` dalam detik (bukan menit!)
2. ✅ Format number bukan string: `60` bukan `"60"`
## 📊 Performa
- **Check interval**: 60 detik
- **Detection time**: < 1 detik
- **Queue processing**: Sequential (1 job at a time)
- **Max schedules tested**: 50 jadwal
- **Memory impact**: Minimal (~5MB per 10 jadwal)
## 🚀 Deploy
Setelah edit struktur Firebase:
1. **Worker otomatis detect** jadwal baru (dalam 60 detik)
2. **No restart needed**
3. **Check logs** untuk verifikasi
Push perubahan worker.js ke Railway:
```bash
cd railway-worker
git add worker.js
git commit -m "feat: Add flexible multi-schedule support"
git push origin main
```
Railway auto-deploy dalam 2-3 menit.
## 📝 Summary
**Flexible**: Tambah jadwal kapanpun tanpa ubah kode
**Scalable**: Support 1-100+ jadwal
**Per-schedule config**: Tiap jadwal bisa beda settingan
**Backward compatible**: Format lama tetap jalan
**Easy to use**: Setup langsung di Firebase
**Production ready**: Sudah include error handling & validation
---
**Need help?** Check logs di Railway atau Firebase console untuk debug.

130
SETUP_FIREBASE.md Normal file
View File

@ -0,0 +1,130 @@
# ⚙️ SETUP RAILWAY WORKER - FIREBASE CREDENTIALS
## 🔑 Cara Mendapatkan Firebase Credentials
### **Step 1: Download Service Account Key**
1. Buka **[Firebase Console](https://console.firebase.google.com)**
2. Pilih project: **project-ta-951b4**
3. Klik ⚙️ icon (Settings) → **Project Settings**
4. Tab **Service Accounts**
5. Klik button **"Generate New Private Key"**
6. Klik **"Generate Key"** → File JSON akan terdownload
---
### **Step 2: Extract Values dari JSON**
Buka file JSON yang didownload (misalnya: `project-ta-951b4-xxxxx.json`) dengan text editor.
**Copy values berikut:**
```json
{
"type": "service_account",
"project_id": "project-ta-951b4", ← COPY INI
"client_email": "firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com", ← COPY INI
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADA...\n-----END PRIVATE KEY-----\n", ← COPY INI
...
}
```
---
### **Step 3: Update File .env**
Edit file: `railway-worker/.env`
```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-----\nMIIEvQIBADA...\n-----END PRIVATE KEY-----\n"
FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com
```
**⚠️ PENTING untuk PRIVATE_KEY:**
- Harus dalam quotes `"`
- Harus ada `\n` (backslash-n), bukan newline sebenarnya
- Copy langsung dari JSON file (sudah format benar)
---
### **Step 4: Test Lokal (Optional)**
```powershell
cd railway-worker
node test-firebase-connection.js
```
**Expected output:**
```
✅ Firebase initialized
✅ DATA FOUND! Structure:
✅ DATA STRUCTURE VALID
```
---
### **Step 5: Set di Railway**
**Railway Dashboard → Service myreppril → Variables tab:**
Add 4 variables (tanpa quotes!):
| 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-----\nMIIEvQIBADA...\n-----END PRIVATE KEY-----\n` |
| `FIREBASE_DATABASE_URL` | `https://project-ta-951b4-default-rtdb.firebaseio.com` |
**Railway akan auto-redeploy setelah add variables!**
---
## 🚀 Setelah Setup
### **Check Railway Logs:**
Railway → Service myreppril → Tab **Logs**
**Harus muncul:**
```
🚀 Starting ApsGo Railway Worker...
📡 Firebase Project: project-ta-951b4
✅ Firebase Admin initialized
✅ Redis connected
✅ Waktu Mode scheduler started
```
---
## ⚠️ Troubleshooting
### **Error: "Firebase initialization failed"**
- Check PRIVATE_KEY format (harus dengan `\n`)
- Pastikan tidak ada extra spaces atau quotes di Railway Variables
- Regenerate key jika perlu
### **Error: "Redis connection failed"**
- Add Redis database di Railway (+ New → Database → Redis)
- Tunggu auto-inject variables
### **No logs at all in Railway**
- Set Root Directory = `railway-worker`
- Check Deployments tab untuk build errors
---
## 📞 Next Steps
Setelah .env file ready:
1. Test lokal: `node test-firebase-connection.js`
2. Set variables di Railway
3. Wait for auto-redeploy
4. Check Railway logs
**Jika masih stuck, screenshot:**
- Railway Variables tab
- Railway Deployments status
- Error message (if any)

324
URGENT_FIX.md Normal file
View File

@ -0,0 +1,324 @@
# 🚨 PERBAIKAN URGENT - Log Scheduler Tidak Muncul
## ❌ Bug yang Ditemukan (Dari Screenshot Railway):
### 1. **Kondisi Logging Terlalu Ketat**
```javascript
// ❌ SALAH - Tidak akan log dengan benar
if (now.getMinutes() % 5 === 0 && now.getSeconds() < 60)
```
- Interval check 60 detik bisa jatuh di detik berapa saja
- Kondisi ini hampir tidak pernah terpenuhi
- **HASIL: Log TIME CHECK tidak pernah muncul!**
### 2. **Tidak Ada Konfirmasi Check Berjalan**
- Fungsi `checkScheduledWatering()` jalan setiap 60 detik
- Tapi **TIDAK ADA LOG** untuk konfirmasi
- **HASIL: Tidak tahu apakah worker benar-benar check jadwal!**
### 3. **Worker Start Terlambat**
- Dari screenshot: Worker start jam **10:20:05**
- Jadwal `waktu_1: "10:18"` sudah lewat **2 menit** sebelumnya
- First check baru jalan setelah 60 detik = **10:21:05**
- **HASIL: Jadwal yang dekat dengan startup time akan terlewat!**
---
## ✅ Solusi yang Sudah Diterapkan:
### 1. **Verbose Logging - Setiap Check Tercatat**
```javascript
// ✅ SEKARANG - Log setiap kali check
⏱️ CHECK #1: 10:20:15 | Mode: ✅
⏱️ CHECK #2: 10:21:15 | Mode: ✅
⏱️ CHECK #3: 10:22:15 | Mode: ✅
```
### 2. **Detail Log Setiap 3 Check atau 5 Menit**
```javascript
⏱️ CHECK #3: 10:22:15 | Mode: ✅
📅 Date: 2026-02-11
🕐 Current: 10:22 (11/2/2026, 10.22.15)
Mode Waktu: ✅ ENABLED
Jadwal 1: 10:18
Jadwal 2: 09:00
```
### 3. **First Check Immediate (5 detik setelah start)**
```javascript
setTimeout(() => {
console.log('🚀 Running first schedule check immediately...');
checkScheduledWatering();
}, 5000);
```
### 4. **Logging Firebase Update yang Detail**
```javascript
🔛 Turning ON: mosvet_1, mosvet_2, mosvet_3...
📌 Firebase path: aktuator
📝 Updates: {
"mosvet_1": true,
"mosvet_2": true,
...
}
✅ Firebase update successful
🔍 Verified aktuator state: { ... }
```
### 5. **Queue Status Monitoring**
```javascript
🕐 JADWAL 1 TRIGGERED: 10:25
🎯 Attempting to add job to queue...
✅ Successfully added to queue: jadwal_1_...
📊 Queue status: 1 active, 0 waiting
```
---
## 🎯 Yang Harus Dilakukan SEKARANG:
### Step 1: Redeploy Railway Worker ⚡
1. Buka **Railway Dashboard**
2. Pilih service **myreppril** (worker)
3. Klik **Deploy** (atau tunggu auto-deploy dari GitHub)
4. Tunggu status jadi **Active**
### Step 2: Monitor Log Real-time 👀
1. Klik tab **Logs** di Railway
2. Dalam **5 detik** pertama harus muncul:
```
🔧 RUNNING DIAGNOSTIC CHECKS...
🕐 CURRENT TIME ANALYSIS:
```
3. Dalam **10 detik** harus muncul:
```
🚀 Running first schedule check immediately...
⏱️ CHECK #1: HH:MM:SS | Mode: ✅
```
4. Setiap **60 detik** harus muncul:
```
⏱️ CHECK #2: HH:MM:SS | Mode: ✅
⏱️ CHECK #3: HH:MM:SS | Mode: ✅
```
### Step 3: Test dengan Jadwal +2 Menit 🧪
1. Lihat log Railway untuk cek waktu sekarang:
```
🕐 Current: 10:30
```
2. Buka **Firebase Console** → Realtime Database
3. Edit node `kontrol/waktu_1`
4. Set ke waktu **sekarang + 2 menit**
- Contoh: Sekarang 10:30 → set `"10:32"`
- **PENTING:** Format harus 2 digit (`"10:32"` bukan `"10:32:00"`)
5. Tunggu 2 menit sambil monitor log Railway
6. Saat waktu match, HARUS muncul:
```
⏱️ CHECK #X: 10:32:YY | Mode: ✅
Jadwal 1: 10:32 🔔 MATCH!
🕐 JADWAL 1 TRIGGERED: 10:32
🎯 Attempting to add job to queue...
✅ Successfully added to queue: jadwal_1_...
📊 Queue status: 1 active, 0 waiting
💧 Processing Job: jadwal_1_...
Type: waktu_jadwal_1
Pots: [1, 2, 3, 4, 5]
Duration: 300s
🔛 Turning ON: mosvet_1, mosvet_2, ...
✅ Firebase update successful
```
### Step 4: Verifikasi di Firebase 🔍
Saat job running, buka Firebase Console dan cek node `/aktuator`:
- `mosvet_1` harus jadi `true` (Pompa Air)
- `mosvet_2` harus jadi `true` (Pompa Pupuk)
- `mosvet_3` sampai `mosvet_7` harus jadi `true` (Pot 1-5)
Setelah durasi selesai:
- Semua `mosvet_X` harus kembali jadi `false`
---
## 🚨 Red Flags - Jika Masih Bermasalah:
### ❌ Log CHECK tidak muncul sama sekali
**Kemungkinan:** Worker crash saat init
**Action:** Screenshot error di Railway → share
### ❌ CHECK muncul tapi mode: ❌
**Kemungkinan:** `kontrol/waktu` di Firebase = `false`
**Action:** Set `kontrol/waktu = true` di Firebase
### ❌ MATCH tapi tidak TRIGGERED
**Kemungkinan:** Queue error atau Redis disconnect
**Action:** Check Redis service status
### ❌ TRIGGERED tapi tidak Processing
**Kemungkinan:** BullMQ worker error
**Action:** Check error log di Railway setelah TRIGGERED
### ❌ Processing tapi aktuator tidak ON
**Kemungkinan:** Firebase update gagal atau permission error
**Action:** Check "Firebase update successful" di log
---
## 📊 Ekspektasi Log Lengkap:
```
// ===== STARTUP (detik 0-10) =====
10:20:05 ✨ ApsGo Railway Worker is running!
10:20:05 ⏰ Timezone: Asia/Jakarta (Current: 11/2/2026, 10.20.05)
10:20:05 ✅ Firebase Admin initialized
10:20:05 ✅ Redis connected
10:20:05 💓 Heartbeat: Worker alive for 0h 0m
10:20:08 🔧 RUNNING DIAGNOSTIC CHECKS...
10:20:08 🕐 CURRENT TIME ANALYSIS:
10:20:08 Server Local: Wed Feb 11 2026 10:20:08 GMT+0700
10:20:08 Asia/Jakarta: 11/2/2026, 10.20.08
10:20:08 HH:MM Format: 10:20
10:20:08 📋 FIREBASE KONTROL:
10:20:08 Mode Waktu: ENABLED ✅
10:20:08 Waktu 1: 10:25
10:20:08 Waktu 2: 18:00
10:20:08 🔍 CHECKING AKTUATOR NODE...
10:20:08 ✅ Aktuator node exists:
10:20:08 mosvet_1: false
10:20:08 mosvet_2: false
... (sampai mosvet_8)
10:20:13 🚀 Running first schedule check immediately...
10:20:13 ⏱️ CHECK #1: 10:20:13 | Mode: ✅
// ===== CHECK RUTIN (setiap 60 detik) =====
10:21:13 ⏱️ CHECK #2: 10:21:13 | Mode: ✅
10:22:13 ⏱️ CHECK #3: 10:22:13 | Mode: ✅
10:22:13 📅 Date: 2026-02-11
10:22:13 🕐 Current: 10:22 (11/2/2026, 10.22.13)
10:22:13 Mode Waktu: ✅ ENABLED
10:22:13 Jadwal 1: 10:25
10:22:13 Jadwal 2: 18:00
10:23:13 ⏱️ CHECK #4: 10:23:13 | Mode: ✅
10:24:13 ⏱️ CHECK #5: 10:24:13 | Mode: ✅
// ===== JADWAL MATCH =====
10:25:13 ⏱️ CHECK #6: 10:25:13 | Mode: ✅
10:25:13 📅 Date: 2026-02-11
10:25:13 🕐 Current: 10:25 (11/2/2026, 10.25.13)
10:25:13 Mode Waktu: ✅ ENABLED
10:25:13 Jadwal 1: 10:25 🔔 MATCH!
10:25:13 Jadwal 2: 18:00
10:25:13 🕐 JADWAL 1 TRIGGERED: 10:25
10:25:13 🎯 Attempting to add job to queue...
10:25:13 ✅ Successfully added to queue: jadwal_1_2026-02-11_10:25
10:25:13 📊 Queue status: 1 active, 0 waiting
// ===== JOB EXECUTION =====
10:25:14 💧 Processing Job: jadwal_1_2026-02-11_10:25
10:25:14 Type: waktu_jadwal_1
10:25:14 Pots: [1, 2, 3, 4, 5]
10:25:14 Duration: 300s
10:25:14 🔛 Turning ON: mosvet_1, mosvet_2, mosvet_3, mosvet_4, mosvet_5, mosvet_6, mosvet_7
10:25:14 📌 Firebase path: aktuator
10:25:14 📝 Updates: {
10:25:14 "mosvet_1": true,
10:25:14 "mosvet_2": true,
10:25:14 "mosvet_3": true,
10:25:14 "mosvet_4": true,
10:25:14 "mosvet_5": true,
10:25:14 "mosvet_6": true,
10:25:14 "mosvet_7": true
10:25:14 }
10:25:14 ✅ Firebase update successful
10:25:14 🔍 Verified aktuator state: {...}
10:25:14 ⏳ 300s remaining...
10:25:24 ⏳ 290s remaining...
10:25:34 ⏳ 280s remaining...
... (setiap 10 detik)
// ===== JOB COMPLETE =====
10:30:14 ⏳ 5s remaining...
10:30:14 🔴 Turning OFF: mosvet_1, mosvet_2, ...
10:30:14 ✅ Aktuators turned OFF successfully
10:30:14 📊 History logged: 2026-02-11 10:25
10:30:14 ✅ Job completed successfully
10:30:14 ✅ Worker completed job jadwal_1_2026-02-11_10:25
// ===== NEXT CHECK =====
10:31:13 ⏱️ CHECK #7: 10:31:13 | Mode: ✅
10:31:13 ⏭️ Jadwal 1 already triggered: jadwal_1_2026-02-11_10:25
```
---
## ✅ Checklist Sukses:
Centang setelah verify di Railway logs:
- [ ] Worker start tanpa error
- [ ] Muncul DIAGNOSTIC CHECKS dalam 5 detik
- [ ] Muncul first CHECK #1 dalam 10 detik
- [ ] CHECK counter naik setiap 60 detik (#2, #3, #4...)
- [ ] Detail log muncul setiap 3 check
- [ ] Mode Waktu menunjukkan ✅ ENABLED
- [ ] Test jadwal +2 menit → muncul 🔔 MATCH!
- [ ] Muncul JADWAL TRIGGERED
- [ ] Muncul "Successfully added to queue"
- [ ] Muncul "Processing Job"
- [ ] Muncul "Firebase update successful"
- [ ] Aktuator berubah di Firebase Console
- [ ] Countdown berjalan setiap 10 detik
- [ ] Aktuator OFF setelah durasi
- [ ] History ter-log
- [ ] Job completed successfully
---
## 📌 File yang Sudah Di-Update:
1. ✅ `worker.js` - Verbose logging + bug fixes
2. ✅ `DEBUGGING_LOGS.md` - Panduan lengkap debugging
3. ✅ `URGENT_FIX.md` - File ini (ringkasan untuk quick action)
**GitHub Repo:** https://github.com/awisnuu/myreppril.git
**Branch:** main
**Latest Commits:**
- `025c39a` - Add comprehensive debugging guide
- `76bb751` - Fix: Add verbose logging for debugging
- `8b8ad29` - Add debugging functions
---
## 🆘 Jika Masih Stuck:
**Share ini ke developer:**
1. Screenshot Railway logs (10 menit terakhir)
2. Screenshot Firebase `/kontrol` node
3. Screenshot Firebase `/aktuator` node
4. Catat waktu test: "Set jadwal 10:32, tunggu sampai 10:33"
5. File log lengkap (download dari Railway)
**Expected vs Actual:**
- Expected: Log CHECK muncul setiap menit
- Actual: [apa yang terjadi]
---
## 🎉 Good Luck!
Worker sekarang sudah **SANGAT VERBOSE** untuk debugging.
Setiap step tercatat dengan detail.
Tidak akan ada lagi misteri "kenapa tidak jalan"!
**Redeploy sekarang dan monitor lognya! 🚀**

54
check-queue.js Normal file
View File

@ -0,0 +1,54 @@
/**
* Script untuk cek status Redis Queue Railway
* Usage: node check-queue.js
*/
require('dotenv').config();
const { Queue } = require('bullmq');
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
password: process.env.REDIS_PASSWORD,
maxRetriesPerRequest: null,
});
const wateringQueue = new Queue('watering', { connection: redis });
async function checkQueue() {
try {
console.log('🔍 Checking Railway Queue Status...\n');
const waiting = await wateringQueue.getWaitingCount();
const active = await wateringQueue.getActiveCount();
const completed = await wateringQueue.getCompletedCount();
const failed = await wateringQueue.getFailedCount();
console.log('📊 Queue Statistics:');
console.log(` ⏳ Waiting: ${waiting}`);
console.log(` ⚙️ Active: ${active}`);
console.log(` ✅ Completed: ${completed}`);
console.log(` ❌ Failed: ${failed}`);
// Get recent jobs
console.log('\n📋 Recent Completed Jobs:');
const completedJobs = await wateringQueue.getCompleted(0, 4);
for (const job of completedJobs) {
console.log(`\n Job ID: ${job.id}`);
console.log(` Type: ${job.data.type}`);
console.log(` Pots: [${job.data.potNumbers.join(', ')}]`);
console.log(` Duration: ${job.data.duration}s`);
console.log(` Time: ${new Date(job.finishedOn).toLocaleString('id-ID')}`);
}
console.log('\n✅ Check complete!');
process.exit(0);
} catch (error) {
console.error('❌ Error:', error.message);
process.exit(1);
}
}
checkQueue();

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

115
debug.js Normal file
View File

@ -0,0 +1,115 @@
/**
* Debug Script untuk Railway Worker
* Jalankan script ini untuk debugging masalah scheduling
*/
require('dotenv').config();
const admin = require('firebase-admin');
// Setup Firebase
const config = {
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
databaseURL: process.env.FIREBASE_DATABASE_URL,
};
console.log('🔍 DEBUG: Railway Worker Troubleshooting');
console.log('==========================================');
async function debugWorker() {
try {
// Test Firebase connection
console.log('1. Testing Firebase connection...');
admin.initializeApp({
credential: admin.credential.cert(config),
databaseURL: config.databaseURL,
});
const db = admin.database();
console.log('✅ Firebase initialized');
// Test reading kontrol config
console.log('\n2. Reading kontrol configuration...');
const snapshot = await db.ref('kontrol').once('value');
const kontrolConfig = snapshot.val();
console.log('📊 Current kontrol config:', JSON.stringify(kontrolConfig, null, 2));
// Check important fields
console.log('\n3. Validating schedule configuration...');
if (!kontrolConfig) {
console.log('❌ No kontrol config found! Flutter app needs to set schedule first.');
return;
}
const issues = [];
if (!kontrolConfig.waktu) {
issues.push('⚠️ waktu mode is disabled');
}
if (!kontrolConfig.waktu_1 && !kontrolConfig.waktu_2) {
issues.push('❌ No jadwal waktu_1 or waktu_2 configured');
}
if (!kontrolConfig.durasi_1 && !kontrolConfig.durasi_2) {
issues.push('❌ No durasi_1 or durasi_2 configured');
}
if (issues.length === 0) {
console.log('✅ All configuration looks good!');
// Test current time vs scheduled time
console.log('\n4. Time check...');
const now = new Date();
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
console.log(`⏰ Current time: ${currentTime}`);
console.log(`📅 Jadwal 1: ${kontrolConfig.waktu_1 || 'Not set'}`);
console.log(`📅 Jadwal 2: ${kontrolConfig.waktu_2 || 'Not set'}`);
if (kontrolConfig.waktu_1 === currentTime || kontrolConfig.waktu_2 === currentTime) {
console.log('🎯 MATCHED! Schedule should trigger now');
} else {
console.log('⏳ No schedule match at current time');
}
} else {
console.log('❌ Configuration issues found:');
issues.forEach(issue => console.log(` ${issue}`));
}
// Test Firebase write permission
console.log('\n5. Testing Firebase write permission...');
await db.ref('debug').set({
timestamp: Date.now(),
message: 'Railway Worker debug test'
});
console.log('✅ Firebase write permission OK');
console.log('\n🎉 Debug completed!');
} catch (error) {
console.error('❌ Debug failed:', error.message);
if (error.message.includes('private_key')) {
console.error('\n💡 Fix: Check FIREBASE_PRIVATE_KEY format');
console.error(' - Should start with "-----BEGIN PRIVATE KEY-----"');
console.error(' - Should have \\n properly escaped');
console.error(' - Should be wrapped in quotes');
}
if (error.message.includes('projectId')) {
console.error('\n💡 Fix: Check FIREBASE_PROJECT_ID');
console.error(' - Should match your Firebase project ID');
}
} finally {
if (admin.apps.length > 0) {
await admin.app().delete();
}
process.exit(0);
}
}
debugWorker();

93
extract-credentials.js Normal file
View File

@ -0,0 +1,93 @@
/**
* Helper script untuk extract Firebase credentials dari Service Account JSON
* dan generate .env file yang benar
*/
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('🔥 Firebase Credentials Extractor\n');
console.log('This tool will help you create a valid .env file from Firebase Service Account JSON.\n');
rl.question('Enter path to your Firebase Service Account JSON file: ', (jsonPath) => {
try {
// Read and parse JSON
if (!fs.existsSync(jsonPath)) {
console.error('❌ File not found:', jsonPath);
console.error('\nTip: Drag and drop the JSON file to this terminal to get its path.');
process.exit(1);
}
console.log('\n📖 Reading JSON file...');
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
const credentials = JSON.parse(jsonContent);
// Validate required fields
const required = ['project_id', 'private_key', 'client_email'];
const missing = required.filter(field => !credentials[field]);
if (missing.length > 0) {
console.error('❌ Missing required fields in JSON:', missing.join(', '));
process.exit(1);
}
console.log('✅ JSON file parsed successfully');
console.log(` Project ID: ${credentials.project_id}`);
console.log(` Client Email: ${credentials.client_email}`);
// Extract database URL
const databaseURL = credentials.databaseURL ||
`https://${credentials.project_id}-default-rtdb.firebaseio.com`;
// Generate .env content
const envContent = `# Firebase Configuration
# Generated from Service Account JSON on ${new Date().toISOString()}
FIREBASE_PROJECT_ID=${credentials.project_id}
FIREBASE_CLIENT_EMAIL=${credentials.client_email}
FIREBASE_PRIVATE_KEY="${credentials.private_key}"
FIREBASE_DATABASE_URL=${databaseURL}
# Redis Configuration (Railway will auto-provide these)
# REDIS_HOST=localhost
# REDIS_PORT=6379
# REDIS_PASSWORD=
`;
// Write to .env
const envPath = path.join(__dirname, '.env');
fs.writeFileSync(envPath, envContent);
console.log('\n✅ .env file created successfully!');
console.log(` Location: ${envPath}`);
console.log('\n📋 Next steps:');
console.log(' 1. Test the connection:');
console.log(' node test-firebase-direct.js');
console.log(' 2. If successful, deploy to Railway');
console.log(' 3. Add same variables to Railway Dashboard');
rl.close();
} catch (error) {
console.error('❌ Error:', error.message);
if (error instanceof SyntaxError) {
console.error('\n💡 The file is not valid JSON. Make sure you downloaded the');
console.error(' Service Account Key from Firebase Console correctly.');
}
rl.close();
process.exit(1);
}
});
rl.on('close', () => {
process.exit(0);
});

34
firebase-example.json Normal file
View File

@ -0,0 +1,34 @@
{
"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
},
"jadwal_3": {
"aktif": true,
"waktu": "16:00",
"durasi": 30,
"pot_aktif": [1, 2, 3, 4, 5],
"pompa_air": true,
"pompa_pupuk": true
}
}
}

105
migrate-history-source.js Normal file
View File

@ -0,0 +1,105 @@
require('dotenv').config();
const admin = require('firebase-admin');
function inferSource(entry) {
const rawSource = (entry && entry.source ? String(entry.source) : '').trim().toLowerCase();
if (rawSource === 'server' || rawSource === 'worker') return 'server';
if (rawSource === 'app' || rawSource === 'mobile') return 'app';
const rawType = (entry && entry.type ? String(entry.type) : '').trim();
if (rawType.length > 0) return 'server';
return 'app';
}
async function main() {
const isDryRun = process.argv.includes('--dry-run');
const requiredEnvs = [
'FIREBASE_PROJECT_ID',
'FIREBASE_CLIENT_EMAIL',
'FIREBASE_PRIVATE_KEY',
'FIREBASE_DATABASE_URL',
];
const missing = requiredEnvs.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing env vars: ${missing.join(', ')}`);
}
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
}),
databaseURL: process.env.FIREBASE_DATABASE_URL,
});
const db = admin.database();
const snap = await db.ref('history').get();
if (!snap.exists()) {
console.log('No history node found. Nothing to migrate.');
await admin.app().delete();
return;
}
const history = snap.val();
const updates = {};
let totalEntries = 0;
let changedEntries = 0;
let serverAssigned = 0;
let appAssigned = 0;
for (const dateKey of Object.keys(history)) {
const dateNode = history[dateKey];
if (!dateNode || typeof dateNode !== 'object') continue;
for (const timeKey of Object.keys(dateNode)) {
const entry = dateNode[timeKey];
if (!entry || typeof entry !== 'object') continue;
totalEntries += 1;
const target = inferSource(entry);
const currentRaw = entry.source == null ? '' : String(entry.source).trim().toLowerCase();
const current = currentRaw === 'worker' ? 'server' : currentRaw === 'mobile' ? 'app' : currentRaw;
if (current !== target) {
updates[`history/${dateKey}/${timeKey}/source`] = target;
changedEntries += 1;
if (target === 'server') {
serverAssigned += 1;
} else {
appAssigned += 1;
}
}
}
}
console.log('=== History Source Migration ===');
console.log(`Mode: ${isDryRun ? 'DRY RUN' : 'APPLY'}`);
console.log(`Total entries scanned: ${totalEntries}`);
console.log(`Entries to update: ${changedEntries}`);
console.log(`Assign source=server: ${serverAssigned}`);
console.log(`Assign source=app: ${appAssigned}`);
if (!isDryRun && changedEntries > 0) {
await db.ref().update(updates);
console.log('Migration applied successfully.');
} else if (!isDryRun) {
console.log('No updates needed.');
}
await admin.app().delete();
}
main().catch(async (err) => {
console.error('Migration failed:', err.message);
process.exitCode = 1;
try {
await admin.app().delete();
} catch (_) {}
});

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "apsgo-railway-worker",
"version": "1.0.0",
"description": "Background worker for ApsGo IoT automation system",
"main": "worker.js",
"scripts": {
"start": "node worker.js",
"dev": "nodemon worker.js",
"debug": "node debug.js",
"test:firebase": "node test-firebase-direct.js",
"setup": "node extract-credentials.js",
"test": "echo \"No tests yet\" && exit 0"
},
"keywords": ["iot", "automation", "firebase", "worker"],
"author": "ApsGo Team",
"license": "MIT",
"dependencies": {
"firebase-admin": "^12.0.0",
"bullmq": "^5.1.0",
"ioredis": "^5.3.2",
"dotenv": "^16.4.5",
"cron": "^3.1.6"
},
"devDependencies": {
"nodemon": "^3.0.3"
},
"engines": {
"node": ">=18.0.0"
}
}

13
railway.json Normal file
View File

@ -0,0 +1,13 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm install"
},
"deploy": {
"numReplicas": 1,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10,
"startCommand": "npm start"
}
}

119
setup-firebase-jadwal.js Normal file
View File

@ -0,0 +1,119 @@
/**
* Script untuk setup Firebase Jadwal
* Run: node setup-firebase-jadwal.js
*/
require('dotenv').config();
const admin = require('firebase-admin');
// Initialize Firebase Admin
try {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
databaseURL: process.env.FIREBASE_DATABASE_URL,
});
console.log('✅ Firebase Admin initialized');
} catch (error) {
console.error('❌ Firebase initialization failed:', error.message);
process.exit(1);
}
const db = admin.database();
// Data jadwal yang akan di-setup
const kontrolData = {
waktu: true,
sensor: false,
otomatis: true,
batas_atas: 32,
batas_bawah: 11,
durasi_sensor: 600,
mode_sensor: "smart",
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
}
};
async function setupFirebase() {
try {
console.log('\n🚀 Starting Firebase setup...');
console.log('📍 Path: /kontrol_1');
console.log('📋 Data:', JSON.stringify(kontrolData, null, 2));
// Set data ke Firebase di path /kontrol_1
await db.ref('kontrol_1').set(kontrolData);
console.log('\n✅ Firebase setup completed successfully!');
console.log('📊 Summary:');
console.log(` - Path: /kontrol_1`);
console.log(` - Mode Waktu: ${kontrolData.waktu ? 'ENABLED' : 'DISABLED'}`);
console.log(` - Total Jadwal: 5`);
console.log(` - Jadwal Aktif: ${Object.keys(kontrolData).filter(k => k.startsWith('jadwal_') && kontrolData[k].aktif).length}`);
console.log('\n📝 Jadwal yang di-setup:');
Object.keys(kontrolData).forEach(key => {
if (key.startsWith('jadwal_')) {
const jadwal = kontrolData[key];
console.log(` ${jadwal.aktif ? '✅' : '❌'} ${key}: ${jadwal.waktu} → Pot [${jadwal.pot_aktif.join(', ')}] (${jadwal.durasi}s)`);
}
});
console.log('\n🎉 Done! Data sudah tersimpan di Firebase.');
console.log('📱 Sekarang update Flutter app untuk pakai path /kontrol_1');
process.exit(0);
} catch (error) {
console.error('\n❌ Error setting up Firebase:', error);
process.exit(1);
}
}
// Run setup
setupFirebase();

108
setup-firebase-threshold.js Normal file
View File

@ -0,0 +1,108 @@
/**
* Script untuk setup Firebase Threshold System
* Run: node setup-firebase-threshold.js
*/
require('dotenv').config();
const admin = require('firebase-admin');
// Initialize Firebase Admin
try {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
databaseURL: process.env.FIREBASE_DATABASE_URL,
});
console.log('✅ Firebase Admin initialized');
} catch (error) {
console.error('❌ Firebase initialization failed:', error.message);
process.exit(1);
}
const db = admin.database();
// Data threshold yang akan di-setup
// Akan merge dengan data yang sudah ada (jadwal_1, jadwal_2, dll)
const thresholdData = {
threshold_1: {
aktif: true,
batas_bawah: 30, // Kelembaban minimum 30%
batas_atas: 70, // Target kelembaban 70% (untuk smart mode)
durasi: 600, // 10 menit (untuk fixed mode)
smart_mode: true, // Smart mode: siram sampai mencapai batas_atas
pot_aktif: [1, 2, 3], // Pot 1, 2, 3 pakai threshold ini
pompa_air: true,
pompa_pupuk: false
},
threshold_2: {
aktif: true,
batas_bawah: 40, // Kelembaban minimum 40%
batas_atas: 80, // Target kelembaban 80%
durasi: 300, // 5 menit
smart_mode: false, // Fixed mode: siram selama durasi saja
pot_aktif: [4, 5], // Pot 4, 5 pakai threshold ini
pompa_air: true,
pompa_pupuk: true
},
threshold_3: {
aktif: false,
batas_bawah: 25, // Untuk tanaman yang butuh kelembaban lebih tinggi
batas_atas: 75,
durasi: 480, // 8 menit
smart_mode: true,
pot_aktif: [3], // Hanya pot 3
pompa_air: true,
pompa_pupuk: false
}
};
async function setupThreshold() {
try {
console.log('\n📦 Starting Threshold System setup...');
console.log('📍 Target path: /kontrol_1\n');
// Get existing data di kontrol_1
const existingSnapshot = await db.ref('kontrol_1').get();
const existingData = existingSnapshot.val() || {};
console.log('📋 Existing data keys:', Object.keys(existingData).join(', '));
// Merge threshold baru dengan data yang sudah ada
const mergedData = {
...existingData,
...thresholdData
};
// Push ke Firebase
await db.ref('kontrol_1').set(mergedData);
console.log('\n✅ Threshold system setup completed!');
console.log('\n📊 Summary:');
console.log(` Total keys in kontrol_1: ${Object.keys(mergedData).length}`);
console.log(` Jadwal nodes: ${Object.keys(mergedData).filter(k => k.startsWith('jadwal_')).length}`);
console.log(` Threshold nodes: ${Object.keys(mergedData).filter(k => k.startsWith('threshold_')).length}`);
console.log('\n🎯 Threshold details:');
Object.keys(thresholdData).forEach(key => {
const t = thresholdData[key];
console.log(` ${key}: ${t.aktif ? '✅' : '❌'} | ${t.batas_bawah}-${t.batas_atas}% | ${t.smart_mode ? 'Smart' : 'Fixed'} | Pot ${t.pot_aktif.join(',')}`);
});
console.log('\n✨ Firebase updated successfully!');
console.log('🔗 Check: https://console.firebase.google.com/u/0/project/YOUR_PROJECT/database');
process.exit(0);
} catch (error) {
console.error('\n❌ Setup failed:', error.message);
console.error('Stack:', error.stack);
process.exit(1);
}
}
// Run the setup
setupThreshold();

149
test-firebase-connection.js Normal file
View File

@ -0,0 +1,149 @@
/**
* Script untuk TEST koneksi Firebase dan verify data penjadwalan
* Usage: node test-firebase-connection.js
*
* Script ini akan:
* 1. Connect ke Firebase
* 2. Baca data /kontrol/
* 3. Display struktur data dan format
* 4. Verify apakah data penjadwalan ada
*/
require('dotenv').config();
const admin = require('firebase-admin');
console.log('🔍 Testing Firebase Connection & Data Structure...\n');
// Initialize Firebase
try {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
databaseURL: process.env.FIREBASE_DATABASE_URL,
});
console.log('✅ Firebase initialized\n');
} catch (error) {
console.error('❌ Firebase initialization failed:', error.message);
process.exit(1);
}
const db = admin.database();
async function testConnection() {
try {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📊 READING /kontrol/ FROM FIREBASE');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
const snapshot = await db.ref('kontrol').once('value');
const kontrolData = snapshot.val();
if (!kontrolData) {
console.log('❌ NO DATA FOUND at /kontrol/\n');
console.log('💡 Kemungkinan masalah:');
console.log(' 1. Data belum di-set dari Flutter app');
console.log(' 2. Firebase credentials salah');
console.log(' 3. Database URL salah\n');
return;
}
console.log('✅ DATA FOUND! Structure:\n');
console.log(JSON.stringify(kontrolData, null, 2));
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('🔍 VALIDATING WAKTU MODE DATA');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
// Validate Waktu Mode
const checks = {
'waktu (mode enabled)': kontrolData.waktu,
'waktu_1 (jadwal 1)': kontrolData.waktu_1,
'waktu_2 (jadwal 2)': kontrolData.waktu_2,
'durasi_1 (detik)': kontrolData.durasi_1,
'durasi_2 (detik)': kontrolData.durasi_2,
};
let allValid = true;
for (const [key, value] of Object.entries(checks)) {
const exists = value !== undefined && value !== null;
const icon = exists ? '✅' : '❌';
console.log(`${icon} ${key.padEnd(25)} = ${value ?? 'NOT SET'}`);
if (!exists) allValid = false;
}
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('⏰ CURRENT TIME vs SCHEDULE');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
const now = new Date();
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
console.log(`🕐 Current time: ${currentTime}`);
console.log(`📅 Jadwal 1: ${kontrolData.waktu_1 || 'NOT SET'}`);
console.log(`📅 Jadwal 2: ${kontrolData.waktu_2 || 'NOT SET'}`);
if (kontrolData.waktu_1 === currentTime) {
console.log('\n🔥 JADWAL 1 SHOULD TRIGGER NOW!');
} else if (kontrolData.waktu_2 === currentTime) {
console.log('\n🔥 JADWAL 2 SHOULD TRIGGER NOW!');
} else {
console.log('\n⏳ No schedule at current time');
}
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('🎯 DIAGNOSIS RESULT');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
if (allValid && kontrolData.waktu === true) {
console.log('✅ DATA STRUCTURE VALID');
console.log('✅ WAKTU MODE ENABLED');
console.log('\n💡 If Railway logs still empty, check:');
console.log(' 1. Railway root directory = "railway-worker"');
console.log(' 2. Environment variables di Railway');
console.log(' 3. Railway deployment status (Logs tab)');
console.log(' 4. Redis service is running\n');
} else {
console.log('❌ DATA STRUCTURE INCOMPLETE or MODE DISABLED\n');
if (!kontrolData.waktu) {
console.log('⚠️ waktu = false → Mode waktu TIDAK AKTIF');
console.log(' 💡 Aktifkan dari Flutter app!\n');
}
if (!kontrolData.waktu_1 || !kontrolData.waktu_2) {
console.log('⚠️ Jadwal belum di-set');
console.log(' 💡 Set jadwal dari Flutter app!\n');
}
}
// Check Sensor Mode
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('🌡️ SENSOR MODE DATA');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
const sensorChecks = {
'otomatis (mode enabled)': kontrolData.otomatis,
'batas_bawah': kontrolData.batas_bawah,
'batas_atas': kontrolData.batas_atas,
'durasi_sensor': kontrolData.durasi_sensor,
};
for (const [key, value] of Object.entries(sensorChecks)) {
const exists = value !== undefined && value !== null;
const icon = exists ? '✅' : '❌';
console.log(`${icon} ${key.padEnd(25)} = ${value ?? 'NOT SET'}`);
}
console.log('\n✅ Test complete!\n');
process.exit(0);
} catch (error) {
console.error('\n❌ Error:', error.message);
console.error(error);
process.exit(1);
}
}
testConnection();

146
test-firebase-direct.js Normal file
View File

@ -0,0 +1,146 @@
/**
* Test koneksi Firebase langsung dengan credentials
* Untuk memverifikasi Railway Worker bisa akses Firebase
*/
require('dotenv').config();
const admin = require('firebase-admin');
console.log('🔍 Testing Firebase Connection...\n');
// Check environment variables
console.log('1. Checking Environment Variables:');
const requiredVars = ['FIREBASE_PROJECT_ID', 'FIREBASE_CLIENT_EMAIL', 'FIREBASE_PRIVATE_KEY', 'FIREBASE_DATABASE_URL'];
let allPresent = true;
requiredVars.forEach(varName => {
const exists = !!process.env[varName];
console.log(` ${exists ? '✅' : '❌'} ${varName}: ${exists ? 'Set' : 'MISSING'}`);
if (!exists) allPresent = false;
});
if (!allPresent) {
console.error('\n❌ Missing environment variables!');
console.error('📋 Create a .env file with:');
console.error(' FIREBASE_PROJECT_ID=project-ta-951b4');
console.error(' FIREBASE_CLIENT_EMAIL=your-service-account@project-ta-951b4.iam.gserviceaccount.com');
console.error(' FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"');
console.error(' FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com');
process.exit(1);
}
async function testFirebase() {
try {
console.log('\n2. Initializing Firebase Admin SDK...');
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
databaseURL: process.env.FIREBASE_DATABASE_URL,
});
console.log('✅ Firebase Admin SDK initialized');
const db = admin.database();
console.log('\n3. Testing Firebase Read Permission...');
const snapshot = await db.ref('kontrol').once('value');
const kontrolData = snapshot.val();
if (!kontrolData) {
console.error('❌ No data in /kontrol node!');
console.error(' Make sure Flutter app has saved schedule configuration.');
process.exit(1);
}
console.log('✅ Successfully read /kontrol data:');
console.log(JSON.stringify(kontrolData, null, 2));
console.log('\n4. Validating Schedule Configuration...');
const issues = [];
if (kontrolData.waktu !== true) {
issues.push('❌ waktu mode is disabled (waktu: false)');
} else {
console.log('✅ Waktu mode is ENABLED');
}
if (!kontrolData.waktu_1 && !kontrolData.waktu_2) {
issues.push('❌ No schedule times configured (waktu_1 and waktu_2 missing)');
} else {
console.log(`✅ Jadwal 1: ${kontrolData.waktu_1 || 'Not set'}`);
console.log(`✅ Jadwal 2: ${kontrolData.waktu_2 || 'Not set'}`);
}
if (!kontrolData.durasi_1 && !kontrolData.durasi_2) {
issues.push('⚠️ No duration configured');
} else {
console.log(`✅ Durasi 1: ${kontrolData.durasi_1 || 0} seconds`);
console.log(`✅ Durasi 2: ${kontrolData.durasi_2 || 0} seconds`);
}
console.log('\n5. Current Time vs Schedule:');
const now = new Date();
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
console.log(`⏰ Current time: ${currentTime} (${now.toLocaleString('id-ID', {timeZone: 'Asia/Jakarta'})})`);
if (kontrolData.waktu_1 === currentTime) {
console.log('🎯 MATCH! Jadwal 1 should trigger NOW!');
} else if (kontrolData.waktu_2 === currentTime) {
console.log('🎯 MATCH! Jadwal 2 should trigger NOW!');
} else {
console.log(`⏳ No match. Next schedule:`);
if (kontrolData.waktu_1) console.log(` - Jadwal 1 at ${kontrolData.waktu_1}`);
if (kontrolData.waktu_2) console.log(` - Jadwal 2 at ${kontrolData.waktu_2}`);
}
console.log('\n6. Testing Firebase Write Permission...');
await db.ref('_test_worker').set({
timestamp: Date.now(),
message: 'Railway Worker test write',
from: 'test-firebase-direct.js'
});
console.log('✅ Successfully wrote test data to Firebase');
// Cleanup
await db.ref('_test_worker').remove();
console.log('✅ Test data cleaned up');
if (issues.length > 0) {
console.log('\n⚠ Configuration Issues Found:');
issues.forEach(issue => console.log(` ${issue}`));
} else {
console.log('\n✅ All checks passed! Configuration looks good.');
console.log('\n📋 Next Steps:');
console.log(' 1. Deploy Railway Worker with these same credentials');
console.log(' 2. Check Railway logs for confirmation');
console.log(' 3. Worker should trigger at scheduled times');
}
} catch (error) {
console.error('\n❌ Error:', error.message);
if (error.code === 'auth/invalid-credential') {
console.error('\n💡 Fix: Check your Firebase Service Account credentials');
console.error(' 1. Go to Firebase Console → Project Settings → Service Accounts');
console.error(' 2. Generate new private key');
console.error(' 3. Update .env file with new credentials');
} else if (error.message.includes('PERMISSION_DENIED')) {
console.error('\n💡 Fix: Check Firebase Realtime Database Rules');
console.error(' 1. Go to Firebase Console → Realtime Database → Rules');
console.error(' 2. Make sure service account has read/write access');
}
process.exit(1);
} finally {
if (admin.apps.length > 0) {
await admin.app().delete();
}
}
}
testFirebase();

1660
worker.js Normal file

File diff suppressed because it is too large Load Diff