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