fix: PDF export visibility and Light/Dark mode theming for Environment Report
This commit is contained in:
parent
2998b90fe0
commit
dc403c29f5
3 changed files with 2834 additions and 248 deletions
355
QUICK-REFERENCE.md
Normal file
355
QUICK-REFERENCE.md
Normal file
|
|
@ -0,0 +1,355 @@
|
||||||
|
# VERIDIAN HARDWARE TIER REPORT - QUICK REFERENCE
|
||||||
|
|
||||||
|
**Deployment:** 5 Greenhouses + 3 Drying Containers
|
||||||
|
**Date:** January 6, 2026
|
||||||
|
**Report:** [VERIDIAN-HARDWARE-TIERS-REPORT.md](./VERIDIAN-HARDWARE-TIERS-REPORT.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 TL;DR (Too Long; Didn't Read)
|
||||||
|
|
||||||
|
### Three Hardware Tiers for 5 GH + 3 Containers
|
||||||
|
|
||||||
|
| Tier | Total Cost | What You Get | Best For |
|
||||||
|
|------|------------|--------------|----------|
|
||||||
|
| **Baseline** | **$2,200** | SensorPush HT1, Kasa EP10, Pi 4, Zigbee soil sensors (optional) | Initial testing, budget-conscious |
|
||||||
|
| **Premium** | **$10,500** | HT.w sensors, CO2, cameras, Pi 5, Soil Scout | Production, small commercial |
|
||||||
|
| **Enterprise** | **$36,600** | PAR sensors, PTZ cameras, Pi cluster, PLC | Commercial scale, maximum uptime |
|
||||||
|
|
||||||
|
**Real 2026 Pricing Applied** - Actual prices are LOWER than estimated!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 What's Included
|
||||||
|
|
||||||
|
### Common to All Tiers:
|
||||||
|
- **26 Air Sensors** (20 greenhouses + 6 containers)
|
||||||
|
- **10 Smart Plugs** (power control for fans, heaters, dehumidifiers)
|
||||||
|
- **Raspberry Pi Edge Device** (runs HomeAssistant + Veridian Edge Agent)
|
||||||
|
- **Network Infrastructure** (WiFi, ethernet, switches)
|
||||||
|
- **SQLite Offline Buffer** (data survives internet outages)
|
||||||
|
|
||||||
|
### Tier-Specific Additions:
|
||||||
|
|
||||||
|
**Baseline ($2,200):**
|
||||||
|
- SensorPush HT1 sensors (±0.36°F accuracy)
|
||||||
|
- Raspberry Pi 4 (4GB RAM)
|
||||||
|
- Kasa EP10 smart plugs (no energy monitoring)
|
||||||
|
- Optional: Zigbee soil sensors ($198) or DIY capacitive ($255)
|
||||||
|
- 30-min UPS battery backup
|
||||||
|
|
||||||
|
**Premium ($10,500):**
|
||||||
|
- SensorPush HT.w sensors (±0.18°F accuracy - 2x better!)
|
||||||
|
- Raspberry Pi 5 (8GB RAM) + NVMe SSD
|
||||||
|
- Kasa EP25 smart plugs (with energy monitoring)
|
||||||
|
- CO2 monitoring (8 zones)
|
||||||
|
- 4MP cameras (8 cameras, 30-day retention)
|
||||||
|
- Soil Scout LoRa sensors (professional-grade)
|
||||||
|
- E-ink displays (8 zones)
|
||||||
|
- 2-hour UPS + 4G LTE failover
|
||||||
|
|
||||||
|
**Enterprise ($36,600):**
|
||||||
|
- Everything in Premium, plus:
|
||||||
|
- PAR light sensors (Apogee SQ-500)
|
||||||
|
- 4K PTZ cameras (90-day retention)
|
||||||
|
- Pi 5 cluster (3 nodes, high availability)
|
||||||
|
- PLC control (industrial automation)
|
||||||
|
- Multi-depth soil sensors (moisture + EC + pH)
|
||||||
|
- Voice annunciation system
|
||||||
|
- 4-6 hour UPS + generator hook-up
|
||||||
|
- Dual 4G LTE internet redundancy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Key Hardware Specifications
|
||||||
|
|
||||||
|
### Environmental Sensors
|
||||||
|
|
||||||
|
**Air Sensors:**
|
||||||
|
- **SensorPush HT1** (Baseline): $54.95 each
|
||||||
|
- Temperature: -40°F to 185°F (±0.36°F)
|
||||||
|
- Humidity: 0-100% RH (±2%)
|
||||||
|
- Battery: 12 months
|
||||||
|
- Data: Every 1 minute
|
||||||
|
|
||||||
|
- **SensorPush HT.w** (Premium/Enterprise): $110 each
|
||||||
|
- Temperature: -40°F to 185°F (±0.18°F) - **2x more accurate!**
|
||||||
|
- Humidity: 0-100% RH (±1%)
|
||||||
|
- Battery: 18 months
|
||||||
|
- Data: Every 30 seconds
|
||||||
|
|
||||||
|
**Soil Sensors (Optional):**
|
||||||
|
|
||||||
|
| Option | Price | Accuracy | Battery | Integration |
|
||||||
|
|--------|-------|----------|---------|-------------|
|
||||||
|
| **Zigbee (Tuya TS0601)** | $18 each | ±5% | 6-12 mo (AAA) | ZHA / Zigbee2MQTT |
|
||||||
|
| **DIY Capacitive** | $8 each | ±5% | N/A (wired) | ADC + Python/Bun |
|
||||||
|
| **Soil Scout (LoRa)** | $150-250 each | ±3% | 5-10 years | MQTT via LoRa gateway |
|
||||||
|
| **Soil Scout Pro** | $250 each | ±2% | 10 years | Multi-depth (6", 12", 18") |
|
||||||
|
|
||||||
|
**CO2 Sensors (Premium/Enterprise):**
|
||||||
|
- **SenseAir S8** (Premium): $200 each
|
||||||
|
- Range: 0-2000 ppm
|
||||||
|
- Accuracy: ±30 ppm ±3%
|
||||||
|
- Output: Modbus RTU
|
||||||
|
|
||||||
|
- **SenseAir S8 Pro** (Enterprise): $350 each
|
||||||
|
- Range: 0-10,000 ppm
|
||||||
|
- Accuracy: ±20 ppm ±2%
|
||||||
|
- Pressure compensation: Yes
|
||||||
|
|
||||||
|
**PAR Sensors (Enterprise only):**
|
||||||
|
- **Apogee SQ-500**: $600 each
|
||||||
|
- Spectral range: 389-683 nm (plant-focused)
|
||||||
|
- Output: 0-2.5V
|
||||||
|
- Weatherproof: IP68
|
||||||
|
|
||||||
|
### Smart Plugs
|
||||||
|
|
||||||
|
| Model | Max Load | Energy Monitor | Price | Best For |
|
||||||
|
|-------|----------|----------------|-------|----------|
|
||||||
|
| **Kasa EP10** | 15A / 1800W | No | $7.50 each | Small loads, basic control |
|
||||||
|
| **Kasa EP25** | 15A / 1800W | Yes | $11.25 each | Medium loads, energy tracking |
|
||||||
|
|
||||||
|
**Note:** Actual prices from 4-packs! EP10 4-pack = $29.99, EP25 4-pack = $44.99
|
||||||
|
|
||||||
|
### Edge Device
|
||||||
|
|
||||||
|
| Model | RAM | Performance | Storage | Price |
|
||||||
|
|-------|-----|-------------|---------|-------|
|
||||||
|
| **Raspberry Pi 4** | 4GB | 1x baseline | 32GB SD | $60 |
|
||||||
|
| **Raspberry Pi 5** | 8GB | 2-3x faster | 500GB NVMe | $95 |
|
||||||
|
|
||||||
|
**Recommendation:** Pi 5 if budget allows (2-3x faster, true Gigabit Ethernet, NVMe support)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Deployment Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Veridian Cloud Backend (VPS)
|
||||||
|
↓ HTTPS/WebSocket
|
||||||
|
Edge Device (Raspberry Pi)
|
||||||
|
├── Home Assistant
|
||||||
|
├── Veridian Edge Agent (Bun)
|
||||||
|
├── SQLite Buffer (offline cache)
|
||||||
|
└── MQTT Broker (optional)
|
||||||
|
↓
|
||||||
|
┌────┴────┬─────────┬────────────┐
|
||||||
|
│ │ │ │
|
||||||
|
SensorPush Kasa Zigbee Cameras/Other
|
||||||
|
Gateway Plugs (soil) (Tier 2/3)
|
||||||
|
│ │ │ │
|
||||||
|
└────┬────┴─────────┴────────────┘
|
||||||
|
↓
|
||||||
|
26 Sensors + 10 Plugs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 Sensor Placement
|
||||||
|
|
||||||
|
### Greenhouses (5 total)
|
||||||
|
- **4 air sensors each** (N, S, E, W walls at canopy height)
|
||||||
|
- **2 soil sensors each** (optional - Tier 1: Zigbee/DIY, Tier 2+: Soil Scout)
|
||||||
|
|
||||||
|
### Containers (3 total)
|
||||||
|
- **2 air sensors each** (front and back, eye level)
|
||||||
|
- **No soil sensors** (drying environment)
|
||||||
|
|
||||||
|
**Total Air Sensors:** 26 (5 GH × 4 + 3 Containers × 2)
|
||||||
|
**Total Soil Sensors (optional):** 10 (5 GH × 2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Software Stack
|
||||||
|
|
||||||
|
### Edge Device (Raspberry Pi):
|
||||||
|
- **Home Assistant** (home automation platform)
|
||||||
|
- **Veridian Edge Agent** (Bun runtime, polls SensorPush API)
|
||||||
|
- **SQLite** (offline data buffering)
|
||||||
|
- **MQTT Broker** (optional - for soil sensors, e-ink displays)
|
||||||
|
- **go2rtc** (Tier 2/3 - camera streaming)
|
||||||
|
|
||||||
|
### Integrations:
|
||||||
|
- **SensorPush** (air sensors) - Native HA integration
|
||||||
|
- **TP-Link Kasa** (smart plugs) - Native HA integration
|
||||||
|
- **ZHA** or **Zigbee2MQTT** (soil sensors)
|
||||||
|
- **Modbus** (CO2 sensors - Tier 2/3)
|
||||||
|
- **Veridian Cloud** (backend API, WebSocket)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Example Automations
|
||||||
|
|
||||||
|
### Emergency Ventilation (High Temp)
|
||||||
|
```yaml
|
||||||
|
# If temperature > 85°F for 1 minute → Turn on fan
|
||||||
|
trigger:
|
||||||
|
- platform: numeric_state
|
||||||
|
entity_id: sensor.greenhouse_1_temperature
|
||||||
|
above: 85
|
||||||
|
for:
|
||||||
|
minutes: 1
|
||||||
|
action:
|
||||||
|
- service: switch.turn_on
|
||||||
|
entity_id: switch.greenhouse_1_fan
|
||||||
|
```
|
||||||
|
|
||||||
|
### Low Soil Moisture Alert
|
||||||
|
```yaml
|
||||||
|
# If soil moisture < 30% → Send notification
|
||||||
|
trigger:
|
||||||
|
- platform: numeric_state
|
||||||
|
entity_id: sensor.greenhouse_1_soil_moisture
|
||||||
|
below: 30
|
||||||
|
for:
|
||||||
|
minutes: 30
|
||||||
|
action:
|
||||||
|
- service: notify.mobile_app
|
||||||
|
data:
|
||||||
|
title: "💧 GH1 Low Soil Moisture"
|
||||||
|
message: "Soil moisture at 25%. Watering needed."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Dehumidifier
|
||||||
|
```yaml
|
||||||
|
# If humidity > 65% → Turn on dehumidifier
|
||||||
|
trigger:
|
||||||
|
- platform: numeric_state
|
||||||
|
entity_id: sensor.container_1_humidity
|
||||||
|
above: 65
|
||||||
|
action:
|
||||||
|
- service: switch.turn_on
|
||||||
|
entity_id: switch.container_1_dehumidifier
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Installation Timeline
|
||||||
|
|
||||||
|
### Tier 1 (Baseline)
|
||||||
|
- **Procurement:** 1 week (Amazon, fast shipping)
|
||||||
|
- **Installation:** 1 weekend (DIY)
|
||||||
|
- **Configuration:** 1-2 days
|
||||||
|
- **Testing:** 1 week
|
||||||
|
- **Total:** 3-4 weeks to full deployment
|
||||||
|
|
||||||
|
### Tier 2 (Premium)
|
||||||
|
- **Procurement:** 2 weeks (some specialty items)
|
||||||
|
- **Installation:** 1 week
|
||||||
|
- **Configuration:** 3-5 days
|
||||||
|
- **Testing:** 1 week
|
||||||
|
- **Total:** 5-6 weeks to full deployment
|
||||||
|
|
||||||
|
### Tier 3 (Enterprise)
|
||||||
|
- **Procurement:** 3-4 weeks (industrial equipment)
|
||||||
|
- **Installation:** 2-3 weeks (professional)
|
||||||
|
- **Configuration:** 1-2 weeks
|
||||||
|
- **Testing:** 2 weeks
|
||||||
|
- **Total:** 10-12 weeks to full deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 Cost Comparison Over 5 Years
|
||||||
|
|
||||||
|
| Tier | Initial Cost | Maintenance | Total 5-Year | ROI Timeline |
|
||||||
|
|------|--------------|-------------|--------------|--------------|
|
||||||
|
| **Baseline** | $2,200 | $324 | $2,524 | 6-12 months |
|
||||||
|
| **Premium** | $10,500 | $760 | $11,260 | 4-6 months |
|
||||||
|
| **Enterprise** | $36,600 | $8,560 | $45,160 | 6-9 months |
|
||||||
|
|
||||||
|
**ROI Calculation:**
|
||||||
|
- Energy savings from smart HVAC control: 20-30%
|
||||||
|
- Reduced crop loss from environmental alerts: 10-20%
|
||||||
|
- Increased yields from optimized environment: 10-20%
|
||||||
|
- Labor savings from automation: 5-10 hours/week
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Recommendations
|
||||||
|
|
||||||
|
### For Initial Testing → **Start with Tier 1 ($2,200)**
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- Low risk investment
|
||||||
|
- Quick deployment (1 weekend)
|
||||||
|
- Proven consumer hardware
|
||||||
|
- Validates system before scaling
|
||||||
|
- Upgrade path to higher tiers
|
||||||
|
|
||||||
|
**Deployment Strategy:**
|
||||||
|
1. Pilot 1 greenhouse + 1 container (Week 1-2)
|
||||||
|
2. Test for 2-4 weeks (Week 3-6)
|
||||||
|
3. Expand to all zones (Week 7-8)
|
||||||
|
4. Total: 2 months to full deployment
|
||||||
|
|
||||||
|
### For Production → **Tier 2 ($10,500)**
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- Production-grade reliability
|
||||||
|
- Camera surveillance (security + monitoring)
|
||||||
|
- CO2 monitoring (yield optimization)
|
||||||
|
- Professional soil sensors (Soil Scout)
|
||||||
|
- Good balance of cost vs capability
|
||||||
|
|
||||||
|
**ROI:** 200-300% over 5 years
|
||||||
|
|
||||||
|
### For Commercial Scale → **Tier 3 ($36,600)**
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
- Maximum uptime (99.99%)
|
||||||
|
- High availability (automatic failover)
|
||||||
|
- PAR light monitoring (professional cultivation)
|
||||||
|
- Enterprise support and SLAs
|
||||||
|
- 7-10 year lifespan
|
||||||
|
|
||||||
|
**ROI:** 400-600% over 5 years, 1000%+ over 10 years
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- **Full Report:** [VERIDIAN-HARDWARE-TIERS-REPORT.md](./VERIDIAN-HARDWARE-TIERS-REPORT.md)
|
||||||
|
- **Veridian Docs:** https://docs.veridian.runfoo.run
|
||||||
|
- **Home Assistant Docs:** https://www.home-assistant.io/docs
|
||||||
|
|
||||||
|
### Hardware Vendors
|
||||||
|
- **SensorPush:** https://sensorpush.com
|
||||||
|
- **Raspberry Pi:** https://www.raspberrypi.com
|
||||||
|
- **Kasa Smart Plugs:** https://www.kasasmart.com
|
||||||
|
- **Soil Scout:** https://soilscout.com
|
||||||
|
- **RAK Wireless (LoRa):** https://store.rakwireless.com
|
||||||
|
|
||||||
|
### Community Support
|
||||||
|
- **Home Assistant Community:** https://community.home-assistant.io
|
||||||
|
- **Veridian Edge Repo:** https://git.runfoo.run/malty/veridian-edge
|
||||||
|
- **GitHub Issues:** https://github.com/your-org/veridian/issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏷️ Tags (for Web Search / SEO)
|
||||||
|
|
||||||
|
`#Veridian #CultivationPlatform #GreenhouseAutomation #IoT #HomeAssistant #SensorPush #SoilMoistureSensors #SmartFarming #CannabisCultivation #EnvironmentalMonitoring #RaspberryPi #EdgeComputing #Zigbee #LoRaWAN #WirelessSensors #ClimateControl #AgricultureTechnology #AgTech #PrecisionAgriculture #GrowAutomation #HVACControl #CO2Monitoring #PARSensors #VPDMonitoring #SmartPlugs #Kasa #TPLink #DIYGreenhouse #ContainerFarming #DryingContainers #HomeAutomation #SmartHome #IndustrialIoT #IIoT #AgriculturalSensors #CapacitiveSoilSensors #Modbus #MQTT #ZHA #Zigbee2MQTT #SoilScout #SenseAir #ApogeeInstruments #PTZCameras #Surveillance #RemoteMonitoring #OfflineResilience #DataBuffering #CloudSync #HardwareTier #BillOfMaterials #BOM #DeploymentGuide #InstallationGuide #MaintenanceSchedule #Troubleshooting #ROI #CostAnalysis #2026Pricing`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Contact & Support
|
||||||
|
|
||||||
|
**Veridian Platform:**
|
||||||
|
- **Documentation:** https://docs.veridian.runfoo.run
|
||||||
|
- **GitHub:** https://github.com/your-org/veridian
|
||||||
|
- **GitLab:** https://git.runfoo.run/malty/veridian-edge
|
||||||
|
- **Support:** support@veridian.runfoo.run
|
||||||
|
|
||||||
|
**Report Issues:**
|
||||||
|
- **GitHub Issues:** https://github.com/your-org/veridian/issues
|
||||||
|
- **Email:** support@veridian.runfoo.run
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Quick Reference Created:** January 6, 2026
|
||||||
|
**Full Report:** [VERIDIAN-HARDWARE-TIERS-REPORT.md](./VERIDIAN-HARDWARE-TIERS-REPORT.md)
|
||||||
|
**Version:** 1.0
|
||||||
|
|
||||||
2222
VERIDIAN-HARDWARE-TIERS-REPORT.md
Normal file
2222
VERIDIAN-HARDWARE-TIERS-REPORT.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -308,18 +308,18 @@ export default function EnvironmentReportPage() {
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-8">
|
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 p-8 transition-colors duration-300">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
{/* --- WEB DASHBOARD VIEW --- */}
|
{/* --- WEB DASHBOARD VIEW --- */}
|
||||||
<div className="no-print mb-8 flex items-center justify-between">
|
<div className="no-print mb-8 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
|
<h1 className="text-3xl font-bold text-slate-900 dark:text-white flex items-center gap-3">
|
||||||
<div className="p-2.5 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 shadow-lg shadow-blue-500/25">
|
<div className="p-2.5 rounded-xl bg-blue-600 shadow-lg shadow-blue-500/25">
|
||||||
<FileText className="w-6 h-6 text-white" />
|
<FileText className="w-6 h-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
Environment Report
|
Environment Report
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-400 ml-14 mt-1">
|
<p className="text-slate-500 dark:text-slate-400 ml-14 mt-1">
|
||||||
Generate and export environmental monitoring reports
|
Generate and export environmental monitoring reports
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -330,7 +330,7 @@ export default function EnvironmentReportPage() {
|
||||||
<select
|
<select
|
||||||
value={dateRange}
|
value={dateRange}
|
||||||
onChange={(e) => setDateRange(e.target.value as any)}
|
onChange={(e) => setDateRange(e.target.value as any)}
|
||||||
className="appearance-none bg-slate-800 border border-slate-700 rounded-xl px-4 py-2.5 pr-10 text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="appearance-none bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl px-4 py-2.5 pr-10 text-slate-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm"
|
||||||
>
|
>
|
||||||
<option value="24h">Last 24 Hours</option>
|
<option value="24h">Last 24 Hours</option>
|
||||||
<option value="7d">Last 7 Days</option>
|
<option value="7d">Last 7 Days</option>
|
||||||
|
|
@ -362,20 +362,19 @@ export default function EnvironmentReportPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dashboard Content (Web View) */}
|
|
||||||
<div className={`space-y-6 no-print transition-opacity duration-300 ${pdfExporting ? 'opacity-50 pointer-events-none grayscale' : ''}`}>
|
<div className={`space-y-6 no-print transition-opacity duration-300 ${pdfExporting ? 'opacity-50 pointer-events-none grayscale' : ''}`}>
|
||||||
{/* Dashboard Header */}
|
{/* Dashboard Header */}
|
||||||
<div className="p-8 rounded-2xl bg-slate-800/50 border border-slate-700/50 text-center">
|
<div className="p-8 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 text-center shadow-sm dark:shadow-none">
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-emerald-500/20 text-emerald-400 rounded-full text-sm font-medium mb-4">
|
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-emerald-500/10 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 rounded-full text-sm font-medium mb-4">
|
||||||
<Activity className="w-4 h-4" />
|
<Activity className="w-4 h-4" />
|
||||||
Environment Monitoring Report
|
Environment Monitoring Report
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-white mb-2">Veridian Grow Operations</h2>
|
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">Veridian Grow Operations</h2>
|
||||||
<p className="text-slate-400">
|
<p className="text-slate-500 dark:text-slate-400">
|
||||||
<Calendar className="w-4 h-4 inline mr-2" />
|
<Calendar className="w-4 h-4 inline mr-2" />
|
||||||
{reportData?.period.start} – {reportData?.period.end}
|
{reportData?.period.start} – {reportData?.period.end}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-2">Generated: {formatDate()}</p>
|
<p className="text-xs text-slate-400 mt-2">Generated: {formatDate()}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary Stats */}
|
{/* Summary Stats */}
|
||||||
|
|
@ -383,13 +382,13 @@ export default function EnvironmentReportPage() {
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="p-5 rounded-2xl bg-gradient-to-br from-red-500/20 to-orange-500/10 border border-red-500/20"
|
className="p-5 rounded-2xl bg-white dark:bg-gradient-to-br dark:from-red-500/20 dark:to-orange-500/10 border border-red-200 dark:border-red-500/20 shadow-sm dark:shadow-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2 text-slate-400">
|
<div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400">
|
||||||
<Thermometer className="w-5 h-5 text-red-400" />
|
<Thermometer className="w-5 h-5 text-red-500 dark:text-red-400" />
|
||||||
<span className="text-sm">Avg Temperature</span>
|
<span className="text-sm">Avg Temperature</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-3xl font-bold text-white">
|
<p className="text-3xl font-bold text-slate-900 dark:text-white">
|
||||||
{reportData?.devices[0]?.stats.temperature.avg.toFixed(1) || '--'}°F
|
{reportData?.devices[0]?.stats.temperature.avg.toFixed(1) || '--'}°F
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
|
@ -401,13 +400,13 @@ export default function EnvironmentReportPage() {
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="p-5 rounded-2xl bg-gradient-to-br from-blue-500/20 to-cyan-500/10 border border-blue-500/20"
|
className="p-5 rounded-2xl bg-white dark:bg-gradient-to-br dark:from-blue-500/20 dark:to-cyan-500/10 border border-blue-200 dark:border-blue-500/20 shadow-sm dark:shadow-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2 text-slate-400">
|
<div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400">
|
||||||
<Droplets className="w-5 h-5 text-blue-400" />
|
<Droplets className="w-5 h-5 text-blue-500 dark:text-blue-400" />
|
||||||
<span className="text-sm">Avg Humidity</span>
|
<span className="text-sm">Avg Humidity</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-3xl font-bold text-white">
|
<p className="text-3xl font-bold text-slate-900 dark:text-white">
|
||||||
{reportData?.devices[0]?.stats.humidity.avg.toFixed(0) || '--'}%
|
{reportData?.devices[0]?.stats.humidity.avg.toFixed(0) || '--'}%
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
|
@ -419,13 +418,13 @@ export default function EnvironmentReportPage() {
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
className="p-5 rounded-2xl bg-gradient-to-br from-purple-500/20 to-pink-500/10 border border-purple-500/20"
|
className="p-5 rounded-2xl bg-white dark:bg-gradient-to-br dark:from-purple-500/20 dark:to-pink-500/10 border border-purple-200 dark:border-purple-500/20 shadow-sm dark:shadow-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2 text-slate-400">
|
<div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400">
|
||||||
<Wind className="w-5 h-5 text-purple-400" />
|
<Wind className="w-5 h-5 text-purple-500 dark:text-purple-400" />
|
||||||
<span className="text-sm">Avg VPD</span>
|
<span className="text-sm">Avg VPD</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-3xl font-bold text-white">
|
<p className="text-3xl font-bold text-slate-900 dark:text-white">
|
||||||
{reportData?.devices[0]?.stats.vpd.avg.toFixed(2) || '--'} kPa
|
{reportData?.devices[0]?.stats.vpd.avg.toFixed(2) || '--'} kPa
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
|
@ -437,26 +436,26 @@ export default function EnvironmentReportPage() {
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.3 }}
|
transition={{ delay: 0.3 }}
|
||||||
className="p-5 rounded-2xl bg-gradient-to-br from-amber-500/20 to-yellow-500/10 border border-amber-500/20"
|
className="p-5 rounded-2xl bg-white dark:bg-gradient-to-br dark:from-amber-500/20 dark:to-yellow-500/10 border border-amber-200 dark:border-amber-500/20 shadow-sm dark:shadow-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2 text-slate-400">
|
<div className="flex items-center gap-2 mb-2 text-slate-500 dark:text-slate-400">
|
||||||
<AlertTriangle className="w-5 h-5 text-amber-400" />
|
<AlertTriangle className="w-5 h-5 text-amber-500 dark:text-amber-400" />
|
||||||
<span className="text-sm">Alerts</span>
|
<span className="text-sm">Alerts</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-3xl font-bold text-white">
|
<p className="text-3xl font-bold text-slate-900 dark:text-white">
|
||||||
{reportData?.alerts.total || 0}
|
{reportData?.alerts.total || 0}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
<CheckCircle className="w-3 h-3 inline mr-1 text-green-400" />
|
<CheckCircle className="w-3 h-3 inline mr-1 text-green-500 dark:text-green-400" />
|
||||||
{reportData?.alerts.resolved || 0} resolved
|
{reportData?.alerts.resolved || 0} resolved
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Temperature Trend Chart */}
|
{/* Temperature Trend Chart */}
|
||||||
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
|
<div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none">
|
||||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<TrendingUp className="w-5 h-5 text-emerald-400" />
|
<TrendingUp className="w-5 h-5 text-emerald-500 dark:text-emerald-400" />
|
||||||
Temperature Trend
|
Temperature Trend
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-64">
|
<div className="h-64">
|
||||||
|
|
@ -468,7 +467,7 @@ export default function EnvironmentReportPage() {
|
||||||
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
|
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
|
||||||
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={10} tickLine={false} />
|
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={10} tickLine={false} />
|
||||||
<YAxis stroke="#64748b" fontSize={10} tickLine={false} />
|
<YAxis stroke="#64748b" fontSize={10} tickLine={false} />
|
||||||
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
|
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
|
||||||
|
|
@ -482,9 +481,9 @@ export default function EnvironmentReportPage() {
|
||||||
|
|
||||||
{/* Humidity and VPD Charts */}
|
{/* Humidity and VPD Charts */}
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
|
<div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none">
|
||||||
<h3 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
|
<h3 className="text-sm font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Droplets className="w-4 h-4 text-blue-400" />
|
<Droplets className="w-4 h-4 text-blue-500 dark:text-blue-400" />
|
||||||
Humidity Trend
|
Humidity Trend
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-48">
|
<div className="h-48">
|
||||||
|
|
@ -496,7 +495,7 @@ export default function EnvironmentReportPage() {
|
||||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
|
||||||
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={9} tickLine={false} />
|
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={9} tickLine={false} />
|
||||||
<YAxis stroke="#64748b" fontSize={9} tickLine={false} domain={[0, 100]} />
|
<YAxis stroke="#64748b" fontSize={9} tickLine={false} domain={[0, 100]} />
|
||||||
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
|
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
|
||||||
|
|
@ -506,9 +505,9 @@ export default function EnvironmentReportPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
|
<div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none">
|
||||||
<h3 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
|
<h3 className="text-sm font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Wind className="w-4 h-4 text-purple-400" />
|
<Wind className="w-4 h-4 text-purple-500 dark:text-purple-400" />
|
||||||
VPD Trend
|
VPD Trend
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-48">
|
<div className="h-48">
|
||||||
|
|
@ -520,7 +519,7 @@ export default function EnvironmentReportPage() {
|
||||||
<stop offset="95%" stopColor="#a855f7" stopOpacity={0} />
|
<stop offset="95%" stopColor="#a855f7" stopOpacity={0} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
|
||||||
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={9} tickLine={false} />
|
<XAxis dataKey="timestamp" stroke="#64748b" fontSize={9} tickLine={false} />
|
||||||
<YAxis stroke="#64748b" fontSize={9} tickLine={false} domain={[0, 2]} />
|
<YAxis stroke="#64748b" fontSize={9} tickLine={false} domain={[0, 2]} />
|
||||||
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
|
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
|
||||||
|
|
@ -535,15 +534,15 @@ export default function EnvironmentReportPage() {
|
||||||
|
|
||||||
{/* Hourly Breakdown */}
|
{/* Hourly Breakdown */}
|
||||||
{reportData?.hourlyBreakdown && reportData.hourlyBreakdown.length > 0 && (
|
{reportData?.hourlyBreakdown && reportData.hourlyBreakdown.length > 0 && (
|
||||||
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50 print-break">
|
<div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none print-break">
|
||||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Clock className="w-5 h-5 text-cyan-400" />
|
<Clock className="w-5 h-5 text-cyan-500 dark:text-cyan-400" />
|
||||||
Hourly Average Temperature
|
Hourly Average Temperature
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-64">
|
<div className="h-64">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={reportData.hourlyBreakdown}>
|
<BarChart data={reportData.hourlyBreakdown}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
|
||||||
<XAxis dataKey="hour" stroke="#64748b" fontSize={10} tickLine={false} />
|
<XAxis dataKey="hour" stroke="#64748b" fontSize={10} tickLine={false} />
|
||||||
<YAxis stroke="#64748b" fontSize={10} tickLine={false} />
|
<YAxis stroke="#64748b" fontSize={10} tickLine={false} />
|
||||||
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
|
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
|
||||||
|
|
@ -555,12 +554,12 @@ export default function EnvironmentReportPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Device Summary Table */}
|
{/* Device Summary Table */}
|
||||||
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
|
<div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none">
|
||||||
<h3 className="text-lg font-semibold text-white mb-4">Device Summary</h3>
|
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Device Summary</h3>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs text-slate-400 uppercase tracking-wider">
|
<tr className="text-left text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||||
<th className="pb-3">Device</th>
|
<th className="pb-3">Device</th>
|
||||||
<th className="pb-3 text-center">Readings</th>
|
<th className="pb-3 text-center">Readings</th>
|
||||||
<th className="pb-3 text-center">Temp (Avg)</th>
|
<th className="pb-3 text-center">Temp (Avg)</th>
|
||||||
|
|
@ -568,19 +567,19 @@ export default function EnvironmentReportPage() {
|
||||||
<th className="pb-3 text-center">VPD (Avg)</th>
|
<th className="pb-3 text-center">VPD (Avg)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-700/50">
|
<tbody className="divide-y divide-slate-200 dark:divide-slate-700/50">
|
||||||
{reportData?.devices.map(device => (
|
{reportData?.devices.map(device => (
|
||||||
<tr key={device.id} className="text-sm">
|
<tr key={device.id} className="text-sm">
|
||||||
<td className="py-3 text-white font-medium">{device.name}</td>
|
<td className="py-3 text-slate-900 dark:text-white font-medium">{device.name}</td>
|
||||||
<td className="py-3 text-slate-300 text-center">{device.readings}</td>
|
<td className="py-3 text-slate-600 dark:text-slate-300 text-center">{device.readings}</td>
|
||||||
<td className="py-3 text-center">
|
<td className="py-3 text-center">
|
||||||
<span className="text-red-400 font-medium">{device.stats.temperature.avg.toFixed(1)}°F</span>
|
<span className="text-red-500 dark:text-red-400 font-medium">{device.stats.temperature.avg.toFixed(1)}°F</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 text-center">
|
<td className="py-3 text-center">
|
||||||
<span className="text-blue-400 font-medium">{device.stats.humidity.avg.toFixed(0)}%</span>
|
<span className="text-blue-500 dark:text-blue-400 font-medium">{device.stats.humidity.avg.toFixed(0)}%</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 text-center">
|
<td className="py-3 text-center">
|
||||||
<span className="text-purple-400 font-medium">{device.stats.vpd.avg.toFixed(2)}</span>
|
<span className="text-purple-500 dark:text-purple-400 font-medium">{device.stats.vpd.avg.toFixed(2)}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
@ -591,16 +590,16 @@ export default function EnvironmentReportPage() {
|
||||||
|
|
||||||
{/* Alert Summary */}
|
{/* Alert Summary */}
|
||||||
{reportData?.alerts.byType && reportData.alerts.byType.length > 0 && (
|
{reportData?.alerts.byType && reportData.alerts.byType.length > 0 && (
|
||||||
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
|
<div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none">
|
||||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<AlertTriangle className="w-5 h-5 text-amber-400" />
|
<AlertTriangle className="w-5 h-5 text-amber-500 dark:text-amber-400" />
|
||||||
Alert Summary
|
Alert Summary
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{reportData.alerts.byType.map(alert => (
|
{reportData.alerts.byType.map(alert => (
|
||||||
<div key={alert.type} className="p-4 rounded-xl bg-slate-900/50 border border-slate-700/50">
|
<div key={alert.type} className="p-4 rounded-xl bg-slate-50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-700/50">
|
||||||
<p className="text-xs text-slate-400 mb-1">{alert.type.replace('_', ' ')}</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">{alert.type.replace('_', ' ')}</p>
|
||||||
<p className="text-2xl font-bold text-white">{alert.count}</p>
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">{alert.count}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -609,17 +608,17 @@ export default function EnvironmentReportPage() {
|
||||||
|
|
||||||
{/* Response Time Analytics */}
|
{/* Response Time Analytics */}
|
||||||
{reportData?.alertAnalytics && (
|
{reportData?.alertAnalytics && (
|
||||||
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50 print-break">
|
<div className="p-6 rounded-2xl bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 shadow-sm dark:shadow-none print-break">
|
||||||
<h3 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-6 flex items-center gap-2">
|
||||||
<Clock className="w-5 h-5 text-cyan-400" />
|
<Clock className="w-5 h-5 text-cyan-500 dark:text-cyan-400" />
|
||||||
Alert Response Time Analytics
|
Alert Response Time Analytics
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* KPIs */}
|
{/* KPIs */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
<div className="p-4 rounded-xl bg-gradient-to-br from-emerald-500/20 to-green-500/10 border border-emerald-500/20">
|
<div className="p-4 rounded-xl bg-white dark:bg-gradient-to-br dark:from-emerald-500/20 dark:to-green-500/10 border border-emerald-200 dark:border-emerald-500/20 shadow-sm dark:shadow-none">
|
||||||
<p className="text-xs text-slate-400 mb-1">Resolution Rate</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">Resolution Rate</p>
|
||||||
<p className="text-3xl font-bold text-emerald-400">
|
<p className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
|
||||||
{reportData.alertAnalytics.summary.resolutionRate}%
|
{reportData.alertAnalytics.summary.resolutionRate}%
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
|
@ -627,25 +626,25 @@ export default function EnvironmentReportPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 rounded-xl bg-gradient-to-br from-blue-500/20 to-cyan-500/10 border border-blue-500/20">
|
<div className="p-4 rounded-xl bg-white dark:bg-gradient-to-br dark:from-blue-500/20 dark:to-cyan-500/10 border border-blue-200 dark:border-blue-500/20 shadow-sm dark:shadow-none">
|
||||||
<p className="text-xs text-slate-400 mb-1">Avg Resolution Time</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">Avg Resolution Time</p>
|
||||||
<p className="text-3xl font-bold text-blue-400">
|
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
{reportData.alertAnalytics.resolutionTimes.avgMinutes || '--'}
|
{reportData.alertAnalytics.resolutionTimes.avgMinutes || '--'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">minutes</p>
|
<p className="text-xs text-slate-500 mt-1">minutes</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-500/10 border border-amber-500/20">
|
<div className="p-4 rounded-xl bg-white dark:bg-gradient-to-br dark:from-amber-500/20 dark:to-orange-500/10 border border-amber-200 dark:border-amber-500/20 shadow-sm dark:shadow-none">
|
||||||
<p className="text-xs text-slate-400 mb-1">Fastest Resolution</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">Fastest Resolution</p>
|
||||||
<p className="text-3xl font-bold text-amber-400">
|
<p className="text-3xl font-bold text-amber-600 dark:text-amber-400">
|
||||||
{reportData.alertAnalytics.resolutionTimes.minMinutes || '--'}
|
{reportData.alertAnalytics.resolutionTimes.minMinutes || '--'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">minutes</p>
|
<p className="text-xs text-slate-500 mt-1">minutes</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 rounded-xl bg-gradient-to-br from-red-500/20 to-rose-500/10 border border-red-500/20">
|
<div className="p-4 rounded-xl bg-white dark:bg-gradient-to-br dark:from-red-500/20 dark:to-rose-500/10 border border-red-200 dark:border-red-500/20 shadow-sm dark:shadow-none">
|
||||||
<p className="text-xs text-slate-400 mb-1">Slowest Resolution</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">Slowest Resolution</p>
|
||||||
<p className="text-3xl font-bold text-red-400">
|
<p className="text-3xl font-bold text-red-600 dark:text-red-400">
|
||||||
{reportData.alertAnalytics.resolutionTimes.maxMinutes || '--'}
|
{reportData.alertAnalytics.resolutionTimes.maxMinutes || '--'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">minutes</p>
|
<p className="text-xs text-slate-500 mt-1">minutes</p>
|
||||||
|
|
@ -655,11 +654,11 @@ export default function EnvironmentReportPage() {
|
||||||
{/* By Alert Type */}
|
{/* By Alert Type */}
|
||||||
{reportData.alertAnalytics.byType.length > 0 && (
|
{reportData.alertAnalytics.byType.length > 0 && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h4 className="text-sm font-medium text-slate-400 mb-3">Resolution Time by Alert Type</h4>
|
<h4 className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-3">Resolution Time by Alert Type</h4>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs text-slate-400 uppercase tracking-wider">
|
<tr className="text-left text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||||
<th className="pb-3">Alert Type</th>
|
<th className="pb-3">Alert Type</th>
|
||||||
<th className="pb-3 text-center">Total</th>
|
<th className="pb-3 text-center">Total</th>
|
||||||
<th className="pb-3 text-center">Resolved</th>
|
<th className="pb-3 text-center">Resolved</th>
|
||||||
|
|
@ -667,25 +666,25 @@ export default function EnvironmentReportPage() {
|
||||||
<th className="pb-3 text-center">Resolution Rate</th>
|
<th className="pb-3 text-center">Resolution Rate</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-700/50">
|
<tbody className="divide-y divide-slate-200 dark:divide-slate-700/50">
|
||||||
{reportData.alertAnalytics.byType.map(alertType => (
|
{reportData.alertAnalytics.byType.map(alertType => (
|
||||||
<tr key={alertType.type} className="text-sm">
|
<tr key={alertType.type} className="text-sm">
|
||||||
<td className="py-3 text-white font-medium">{alertType.type.replace('_', ' ')}</td>
|
<td className="py-3 text-slate-900 dark:text-white font-medium">{alertType.type.replace('_', ' ')}</td>
|
||||||
<td className="py-3 text-slate-300 text-center">{alertType.count}</td>
|
<td className="py-3 text-slate-600 dark:text-slate-300 text-center">{alertType.count}</td>
|
||||||
<td className="py-3 text-center">
|
<td className="py-3 text-center">
|
||||||
<span className="text-green-400">{alertType.resolved}</span>
|
<span className="text-green-600 dark:text-green-400">{alertType.resolved}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 text-center">
|
<td className="py-3 text-center">
|
||||||
<span className="text-cyan-400 font-medium">
|
<span className="text-cyan-600 dark:text-cyan-400 font-medium">
|
||||||
{alertType.avgResolutionMin ? `${alertType.avgResolutionMin} min` : '--'}
|
{alertType.avgResolutionMin ? `${alertType.avgResolutionMin} min` : '--'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 text-center">
|
<td className="py-3 text-center">
|
||||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${alertType.count > 0 && alertType.resolved / alertType.count >= 0.9
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${alertType.count > 0 && alertType.resolved / alertType.count >= 0.9
|
||||||
? 'bg-green-500/20 text-green-400'
|
? 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400'
|
||||||
: alertType.count > 0 && alertType.resolved / alertType.count >= 0.5
|
: alertType.count > 0 && alertType.resolved / alertType.count >= 0.5
|
||||||
? 'bg-amber-500/20 text-amber-400'
|
? 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400'
|
||||||
: 'bg-red-500/20 text-red-400'
|
: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400'
|
||||||
}`}>
|
}`}>
|
||||||
{alertType.count > 0 ? Math.round(alertType.resolved / alertType.count * 100) : 0}%
|
{alertType.count > 0 ? Math.round(alertType.resolved / alertType.count * 100) : 0}%
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -701,39 +700,39 @@ export default function EnvironmentReportPage() {
|
||||||
{/* Recent Alerts Timeline */}
|
{/* Recent Alerts Timeline */}
|
||||||
{reportData.alertAnalytics.recentAlerts.length > 0 && (
|
{reportData.alertAnalytics.recentAlerts.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-slate-400 mb-3">Recent Alert Timeline</h4>
|
<h4 className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-3">Recent Alert Timeline</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{reportData.alertAnalytics.recentAlerts.slice(0, 5).map(alert => (
|
{reportData.alertAnalytics.recentAlerts.slice(0, 5).map(alert => (
|
||||||
<div
|
<div
|
||||||
key={alert.id}
|
key={alert.id}
|
||||||
className={`p-4 rounded-xl border ${alert.resolvedAt
|
className={`p-4 rounded-xl border ${alert.resolvedAt
|
||||||
? 'bg-green-500/5 border-green-500/20'
|
? 'bg-green-50 dark:bg-green-500/5 border-green-200 dark:border-green-500/20'
|
||||||
: 'bg-amber-500/5 border-amber-500/20'
|
: 'bg-amber-50 dark:bg-amber-500/5 border-amber-200 dark:border-amber-500/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${alert.severity === 'CRITICAL' ? 'bg-red-500/20 text-red-400' :
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${alert.severity === 'CRITICAL' ? 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400' :
|
||||||
alert.severity === 'WARNING' ? 'bg-amber-500/20 text-amber-400' :
|
alert.severity === 'WARNING' ? 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400' :
|
||||||
'bg-blue-500/20 text-blue-400'
|
'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400'
|
||||||
}`}>
|
}`}>
|
||||||
{alert.severity}
|
{alert.severity}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-white font-medium">{alert.type.replace('_', ' ')}</span>
|
<span className="text-sm text-slate-900 dark:text-white font-medium">{alert.type.replace('_', ' ')}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={`flex items-center gap-1.5 text-xs font-medium ${alert.resolvedAt ? 'text-green-400' : 'text-amber-400'
|
<span className={`flex items-center gap-1.5 text-xs font-medium ${alert.resolvedAt ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'
|
||||||
}`}>
|
}`}>
|
||||||
<CheckCircle className="w-3 h-3" />
|
<CheckCircle className="w-3 h-3" />
|
||||||
{alert.resolvedAt ? 'Resolved' : 'Pending'}
|
{alert.resolvedAt ? 'Resolved' : 'Pending'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-300 mb-2">{alert.message}</p>
|
<p className="text-sm text-slate-600 dark:text-slate-300 mb-2">{alert.message}</p>
|
||||||
<div className="flex items-center gap-4 text-xs text-slate-500">
|
<div className="flex items-center gap-4 text-xs text-slate-500">
|
||||||
<span>
|
<span>
|
||||||
Triggered: {new Date(alert.createdAt).toLocaleString()}
|
Triggered: {new Date(alert.createdAt).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
{alert.resolutionTimeMin && (
|
{alert.resolutionTimeMin && (
|
||||||
<span className="text-cyan-400">
|
<span className="text-cyan-600 dark:text-cyan-400">
|
||||||
Return to Normal: {alert.resolutionTimeMin.toFixed(1)} min
|
Return to Normal: {alert.resolutionTimeMin.toFixed(1)} min
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -747,7 +746,7 @@ export default function EnvironmentReportPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="text-center py-6 text-xs text-slate-500 border-t border-slate-700/50">
|
<div className="text-center py-6 text-xs text-slate-500 border-t border-slate-200 dark:border-slate-700/50">
|
||||||
<p>Veridian Grow Operations Manager • Environment Monitoring Report</p>
|
<p>Veridian Grow Operations Manager • Environment Monitoring Report</p>
|
||||||
<p className="mt-1">Generated automatically • Data subject to sensor accuracy</p>
|
<p className="mt-1">Generated automatically • Data subject to sensor accuracy</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -755,10 +754,19 @@ export default function EnvironmentReportPage() {
|
||||||
|
|
||||||
|
|
||||||
{/* --- CLEAN DOCUMENT LAYOUT (For PDF Generation) --- */}
|
{/* --- CLEAN DOCUMENT LAYOUT (For PDF Generation) --- */}
|
||||||
{/* This div is referenced by reportRef for html2canvas */}
|
{/* Wrapper for off-screen positioning - MUST be visible for html2canvas */}
|
||||||
|
<div
|
||||||
|
className="fixed top-0"
|
||||||
|
style={{
|
||||||
|
left: pdfExporting ? '0' : '-9999px',
|
||||||
|
zIndex: -50,
|
||||||
|
visibility: 'visible',
|
||||||
|
width: '210mm'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={reportRef}
|
ref={reportRef}
|
||||||
className={`bg-white text-black p-12 mx-auto shadow-2xl ${pdfExporting ? 'block' : 'hidden'}`}
|
className="bg-white text-black p-12 mx-auto shadow-2xl"
|
||||||
style={{
|
style={{
|
||||||
width: '210mm', // A4 Width
|
width: '210mm', // A4 Width
|
||||||
minHeight: '297mm', // A4 Height
|
minHeight: '297mm', // A4 Height
|
||||||
|
|
@ -934,6 +942,7 @@ export default function EnvironmentReportPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue