Merge production/main into main
This commit is contained in:
commit
b81cc38773
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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! 🚀**
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (_) {}
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
Loading…
Reference in New Issue