fix: PDF export visibility and Light/Dark mode theming for Environment Report
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

This commit is contained in:
fullsizemalt 2026-01-06 03:15:45 -08:00
parent 2998b90fe0
commit dc403c29f5
3 changed files with 2834 additions and 248 deletions

355
QUICK-REFERENCE.md Normal file
View 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

File diff suppressed because it is too large Load diff

View file

@ -308,18 +308,18 @@ export default function EnvironmentReportPage() {
}
`}</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">
{/* --- WEB DASHBOARD VIEW --- */}
<div className="no-print mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold 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">
<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-blue-600 shadow-lg shadow-blue-500/25">
<FileText className="w-6 h-6 text-white" />
</div>
Environment Report
</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
</p>
</div>
@ -330,7 +330,7 @@ export default function EnvironmentReportPage() {
<select
value={dateRange}
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="7d">Last 7 Days</option>
@ -362,20 +362,19 @@ export default function EnvironmentReportPage() {
</div>
</div>
{/* Dashboard Content (Web View) */}
<div className={`space-y-6 no-print transition-opacity duration-300 ${pdfExporting ? 'opacity-50 pointer-events-none grayscale' : ''}`}>
{/* Dashboard Header */}
<div className="p-8 rounded-2xl bg-slate-800/50 border border-slate-700/50 text-center">
<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="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/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" />
Environment Monitoring Report
</div>
<h2 className="text-2xl font-bold text-white mb-2">Veridian Grow Operations</h2>
<p className="text-slate-400">
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">Veridian Grow Operations</h2>
<p className="text-slate-500 dark:text-slate-400">
<Calendar className="w-4 h-4 inline mr-2" />
{reportData?.period.start} {reportData?.period.end}
</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>
{/* Summary Stats */}
@ -383,13 +382,13 @@ export default function EnvironmentReportPage() {
<motion.div
initial={{ opacity: 0, y: 20 }}
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">
<Thermometer className="w-5 h-5 text-red-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-500 dark:text-red-400" />
<span className="text-sm">Avg Temperature</span>
</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
</p>
<p className="text-xs text-slate-500 mt-1">
@ -401,13 +400,13 @@ export default function EnvironmentReportPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
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">
<Droplets className="w-5 h-5 text-blue-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-500 dark:text-blue-400" />
<span className="text-sm">Avg Humidity</span>
</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) || '--'}%
</p>
<p className="text-xs text-slate-500 mt-1">
@ -419,13 +418,13 @@ export default function EnvironmentReportPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
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">
<Wind className="w-5 h-5 text-purple-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-500 dark:text-purple-400" />
<span className="text-sm">Avg VPD</span>
</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
</p>
<p className="text-xs text-slate-500 mt-1">
@ -437,26 +436,26 @@ export default function EnvironmentReportPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
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">
<AlertTriangle className="w-5 h-5 text-amber-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-500 dark:text-amber-400" />
<span className="text-sm">Alerts</span>
</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}
</p>
<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
</p>
</motion.div>
</div>
{/* Temperature Trend Chart */}
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-emerald-400" />
<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-slate-900 dark:text-white mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-emerald-500 dark:text-emerald-400" />
Temperature Trend
</h3>
<div className="h-64">
@ -468,7 +467,7 @@ export default function EnvironmentReportPage() {
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
</linearGradient>
</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} />
<YAxis stroke="#64748b" fontSize={10} tickLine={false} />
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
@ -482,9 +481,9 @@ export default function EnvironmentReportPage() {
{/* Humidity and VPD Charts */}
<div className="grid grid-cols-2 gap-6">
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
<h3 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
<Droplets className="w-4 h-4 text-blue-400" />
<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-slate-900 dark:text-white mb-4 flex items-center gap-2">
<Droplets className="w-4 h-4 text-blue-500 dark:text-blue-400" />
Humidity Trend
</h3>
<div className="h-48">
@ -496,7 +495,7 @@ export default function EnvironmentReportPage() {
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</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} />
<YAxis stroke="#64748b" fontSize={9} tickLine={false} domain={[0, 100]} />
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
@ -506,9 +505,9 @@ export default function EnvironmentReportPage() {
</div>
</div>
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
<h3 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
<Wind className="w-4 h-4 text-purple-400" />
<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-slate-900 dark:text-white mb-4 flex items-center gap-2">
<Wind className="w-4 h-4 text-purple-500 dark:text-purple-400" />
VPD Trend
</h3>
<div className="h-48">
@ -520,7 +519,7 @@ export default function EnvironmentReportPage() {
<stop offset="95%" stopColor="#a855f7" stopOpacity={0} />
</linearGradient>
</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} />
<YAxis stroke="#64748b" fontSize={9} tickLine={false} domain={[0, 2]} />
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
@ -535,15 +534,15 @@ export default function EnvironmentReportPage() {
{/* Hourly Breakdown */}
{reportData?.hourlyBreakdown && reportData.hourlyBreakdown.length > 0 && (
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50 print-break">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Clock className="w-5 h-5 text-cyan-400" />
<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-slate-900 dark:text-white mb-4 flex items-center gap-2">
<Clock className="w-5 h-5 text-cyan-500 dark:text-cyan-400" />
Hourly Average Temperature
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<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} />
<YAxis stroke="#64748b" fontSize={10} tickLine={false} />
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }} />
@ -555,12 +554,12 @@ export default function EnvironmentReportPage() {
)}
{/* Device Summary Table */}
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
<h3 className="text-lg font-semibold text-white mb-4">Device Summary</h3>
<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-slate-900 dark:text-white mb-4">Device Summary</h3>
<div className="overflow-x-auto">
<table className="w-full">
<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 text-center">Readings</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>
</tr>
</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 => (
<tr key={device.id} className="text-sm">
<td className="py-3 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-900 dark:text-white font-medium">{device.name}</td>
<td className="py-3 text-slate-600 dark:text-slate-300 text-center">{device.readings}</td>
<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 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 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>
</tr>
))}
@ -591,16 +590,16 @@ export default function EnvironmentReportPage() {
{/* Alert Summary */}
{reportData?.alerts.byType && reportData.alerts.byType.length > 0 && (
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-amber-400" />
<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-slate-900 dark:text-white mb-4 flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-amber-500 dark:text-amber-400" />
Alert Summary
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{reportData.alerts.byType.map(alert => (
<div key={alert.type} className="p-4 rounded-xl bg-slate-900/50 border border-slate-700/50">
<p className="text-xs text-slate-400 mb-1">{alert.type.replace('_', ' ')}</p>
<p className="text-2xl font-bold text-white">{alert.count}</p>
<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-500 dark:text-slate-400 mb-1">{alert.type.replace('_', ' ')}</p>
<p className="text-2xl font-bold text-slate-900 dark:text-white">{alert.count}</p>
</div>
))}
</div>
@ -609,17 +608,17 @@ export default function EnvironmentReportPage() {
{/* Response Time Analytics */}
{reportData?.alertAnalytics && (
<div className="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50 print-break">
<h3 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
<Clock className="w-5 h-5 text-cyan-400" />
<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-slate-900 dark:text-white mb-6 flex items-center gap-2">
<Clock className="w-5 h-5 text-cyan-500 dark:text-cyan-400" />
Alert Response Time Analytics
</h3>
{/* KPIs */}
<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">
<p className="text-xs text-slate-400 mb-1">Resolution Rate</p>
<p className="text-3xl font-bold text-emerald-400">
<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-500 dark:text-slate-400 mb-1">Resolution Rate</p>
<p className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
{reportData.alertAnalytics.summary.resolutionRate}%
</p>
<p className="text-xs text-slate-500 mt-1">
@ -627,25 +626,25 @@ export default function EnvironmentReportPage() {
</p>
</div>
<div className="p-4 rounded-xl bg-gradient-to-br from-blue-500/20 to-cyan-500/10 border border-blue-500/20">
<p className="text-xs text-slate-400 mb-1">Avg Resolution Time</p>
<p className="text-3xl font-bold text-blue-400">
<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-500 dark:text-slate-400 mb-1">Avg Resolution Time</p>
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{reportData.alertAnalytics.resolutionTimes.avgMinutes || '--'}
</p>
<p className="text-xs text-slate-500 mt-1">minutes</p>
</div>
<div className="p-4 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-500/10 border border-amber-500/20">
<p className="text-xs text-slate-400 mb-1">Fastest Resolution</p>
<p className="text-3xl font-bold text-amber-400">
<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-500 dark:text-slate-400 mb-1">Fastest Resolution</p>
<p className="text-3xl font-bold text-amber-600 dark:text-amber-400">
{reportData.alertAnalytics.resolutionTimes.minMinutes || '--'}
</p>
<p className="text-xs text-slate-500 mt-1">minutes</p>
</div>
<div className="p-4 rounded-xl bg-gradient-to-br from-red-500/20 to-rose-500/10 border border-red-500/20">
<p className="text-xs text-slate-400 mb-1">Slowest Resolution</p>
<p className="text-3xl font-bold text-red-400">
<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-500 dark:text-slate-400 mb-1">Slowest Resolution</p>
<p className="text-3xl font-bold text-red-600 dark:text-red-400">
{reportData.alertAnalytics.resolutionTimes.maxMinutes || '--'}
</p>
<p className="text-xs text-slate-500 mt-1">minutes</p>
@ -655,11 +654,11 @@ export default function EnvironmentReportPage() {
{/* By Alert Type */}
{reportData.alertAnalytics.byType.length > 0 && (
<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">
<table className="w-full">
<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 text-center">Total</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>
</tr>
</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 => (
<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-300 text-center">{alertType.count}</td>
<td className="py-3 text-slate-900 dark:text-white font-medium">{alertType.type.replace('_', ' ')}</td>
<td className="py-3 text-slate-600 dark:text-slate-300 text-center">{alertType.count}</td>
<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 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` : '--'}
</span>
</td>
<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
? '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
? 'bg-amber-500/20 text-amber-400'
: 'bg-red-500/20 text-red-400'
? 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-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}%
</span>
@ -701,39 +700,39 @@ export default function EnvironmentReportPage() {
{/* Recent Alerts Timeline */}
{reportData.alertAnalytics.recentAlerts.length > 0 && (
<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">
{reportData.alertAnalytics.recentAlerts.slice(0, 5).map(alert => (
<div
key={alert.id}
className={`p-4 rounded-xl border ${alert.resolvedAt
? 'bg-green-500/5 border-green-500/20'
: 'bg-amber-500/5 border-amber-500/20'
? 'bg-green-50 dark:bg-green-500/5 border-green-200 dark:border-green-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 gap-2">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${alert.severity === 'CRITICAL' ? 'bg-red-500/20 text-red-400' :
alert.severity === 'WARNING' ? 'bg-amber-500/20 text-amber-400' :
'bg-blue-500/20 text-blue-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-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400' :
'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400'
}`}>
{alert.severity}
</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>
<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" />
{alert.resolvedAt ? 'Resolved' : 'Pending'}
</span>
</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">
<span>
Triggered: {new Date(alert.createdAt).toLocaleString()}
</span>
{alert.resolutionTimeMin && (
<span className="text-cyan-400">
<span className="text-cyan-600 dark:text-cyan-400">
Return to Normal: {alert.resolutionTimeMin.toFixed(1)} min
</span>
)}
@ -747,7 +746,7 @@ export default function EnvironmentReportPage() {
)}
{/* 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 className="mt-1">Generated automatically Data subject to sensor accuracy</p>
</div>
@ -755,181 +754,191 @@ export default function EnvironmentReportPage() {
{/* --- 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
ref={reportRef}
className={`bg-white text-black p-12 mx-auto shadow-2xl ${pdfExporting ? 'block' : 'hidden'}`}
className="fixed top-0"
style={{
width: '210mm', // A4 Width
minHeight: '297mm', // A4 Height
fontFamily: '"Inter", "Helvetica Neue", Arial, sans-serif'
left: pdfExporting ? '0' : '-9999px',
zIndex: -50,
visibility: 'visible',
width: '210mm'
}}
>
{/* Document Header */}
<div className="border-b-2 border-slate-900 pb-6 mb-8 flex justify-between items-end">
<div>
<h1 className="text-4xl font-bold tracking-tight text-slate-900 mb-2">Environment Report</h1>
<p className="text-slate-500 font-medium">Veridian Grow Operations</p>
</div>
<div className="text-right">
<p className="text-sm text-slate-500">Report Period</p>
<p className="font-semibold text-slate-900">{reportData?.period.start} {reportData?.period.end}</p>
<p className="text-xs text-slate-400 mt-1">Generated: {new Date().toLocaleDateString()}</p>
</div>
</div>
{/* Executive Summary Table */}
<div className="mb-10">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Executive Summary
</h2>
<div className="grid grid-cols-4 gap-6 mb-6">
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg Temperature</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.temperature.avg.toFixed(1)}°F</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.temperature.min.toFixed(1)}° / Max: {reportData?.devices[0]?.stats.temperature.max.toFixed(1)}°</span>
<div
ref={reportRef}
className="bg-white text-black p-12 mx-auto shadow-2xl"
style={{
width: '210mm', // A4 Width
minHeight: '297mm', // A4 Height
fontFamily: '"Inter", "Helvetica Neue", Arial, sans-serif'
}}
>
{/* Document Header */}
<div className="border-b-2 border-slate-900 pb-6 mb-8 flex justify-between items-end">
<div>
<h1 className="text-4xl font-bold tracking-tight text-slate-900 mb-2">Environment Report</h1>
<p className="text-slate-500 font-medium">Veridian Grow Operations</p>
</div>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg Humidity</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.humidity.avg.toFixed(0)}%</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.humidity.min.toFixed(0)}% / Max: {reportData?.devices[0]?.stats.humidity.max.toFixed(0)}%</span>
</div>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg VPD</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.vpd.avg.toFixed(2)}</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.vpd.min.toFixed(2)} / Max: {reportData?.devices[0]?.stats.vpd.max.toFixed(2)}</span>
</div>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Total Alerts</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.alertAnalytics?.summary.total || 0}</span>
<span className="text-xs text-slate-500">Resolution Rate: {reportData?.alertAnalytics?.summary.resolutionRate}%</span>
<div className="text-right">
<p className="text-sm text-slate-500">Report Period</p>
<p className="font-semibold text-slate-900">{reportData?.period.start} {reportData?.period.end}</p>
<p className="text-xs text-slate-400 mt-1">Generated: {new Date().toLocaleDateString()}</p>
</div>
</div>
</div>
{/* Historical Charts (Clean Style) */}
<div className="mb-10 page-break-inside-avoid">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Environmental Trends
</h2>
<div className="space-y-8">
<div className="h-64 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">Temperature History (°F)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={10} tickLine={false} tickFormatter={(val) => val.split(',')[1]} />
<YAxis stroke={reportTheme.text} fontSize={10} tickLine={false} domain={['auto', 'auto']} />
<Area type="monotone" dataKey="temperature" stroke={reportTheme.temp} fill={reportTheme.temp} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="h-48 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">Humidity History (%)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={9} tick={false} />
<YAxis stroke={reportTheme.text} fontSize={9} tickLine={false} domain={[0, 100]} />
<Area type="monotone" dataKey="humidity" stroke={reportTheme.humidity} fill={reportTheme.humidity} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
{/* Executive Summary Table */}
<div className="mb-10">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Executive Summary
</h2>
<div className="grid grid-cols-4 gap-6 mb-6">
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg Temperature</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.temperature.avg.toFixed(1)}°F</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.temperature.min.toFixed(1)}° / Max: {reportData?.devices[0]?.stats.temperature.max.toFixed(1)}°</span>
</div>
<div className="h-48 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">VPD History (kPa)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={9} tick={false} />
<YAxis stroke={reportTheme.text} fontSize={9} tickLine={false} domain={[0, 2.5]} />
<Area type="monotone" dataKey="vpd" stroke={reportTheme.vpd} fill={reportTheme.vpd} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg Humidity</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.humidity.avg.toFixed(0)}%</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.humidity.min.toFixed(0)}% / Max: {reportData?.devices[0]?.stats.humidity.max.toFixed(0)}%</span>
</div>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg VPD</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.vpd.avg.toFixed(2)}</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.vpd.min.toFixed(2)} / Max: {reportData?.devices[0]?.stats.vpd.max.toFixed(2)}</span>
</div>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Total Alerts</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.alertAnalytics?.summary.total || 0}</span>
<span className="text-xs text-slate-500">Resolution Rate: {reportData?.alertAnalytics?.summary.resolutionRate}%</span>
</div>
</div>
</div>
</div>
{/* Data Tables Section */}
<div className="mb-8">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Alert & Intervention Analysis
</h2>
{/* Historical Charts (Clean Style) */}
<div className="mb-10 page-break-inside-avoid">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Environmental Trends
</h2>
{/* KPI Table */}
<table className="w-full text-sm text-left border border-slate-200 mb-6">
<thead className="bg-slate-100 text-slate-700 font-semibold uppercase text-xs">
<tr>
<th className="px-4 py-3 border-b border-slate-200">Metric</th>
<th className="px-4 py-3 border-b border-slate-200">Value</th>
<th className="px-4 py-3 border-b border-slate-200">Notes</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
<tr>
<td className="px-4 py-2 font-medium">Avg Resolution Time</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.resolutionTimes.avgMinutes || '--'} min</td>
<td className="px-4 py-2 text-slate-500">Average time to return to normal range</td>
</tr>
<tr>
<td className="px-4 py-2 font-medium">Resolution Rate</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.summary.resolutionRate}%</td>
<td className="px-4 py-2 text-slate-500">{reportData?.alertAnalytics?.summary.resolved} resolved out of {reportData?.alertAnalytics?.summary.total} total</td>
</tr>
<tr>
<td className="px-4 py-2 font-medium">Slowest Response</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.resolutionTimes.maxMinutes || '--'} min</td>
<td className="px-4 py-2 text-slate-500">Max deviation duration</td>
</tr>
</tbody>
</table>
<div className="space-y-8">
<div className="h-64 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">Temperature History (°F)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={10} tickLine={false} tickFormatter={(val) => val.split(',')[1]} />
<YAxis stroke={reportTheme.text} fontSize={10} tickLine={false} domain={['auto', 'auto']} />
<Area type="monotone" dataKey="temperature" stroke={reportTheme.temp} fill={reportTheme.temp} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
{/* Recent Alerts List */}
{reportData?.alertAnalytics?.recentAlerts && reportData.alertAnalytics.recentAlerts.length > 0 ? (
<table className="w-full text-xs text-left border border-slate-200">
<thead className="bg-slate-800 text-white font-semibold uppercase">
<div className="grid grid-cols-2 gap-6">
<div className="h-48 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">Humidity History (%)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={9} tick={false} />
<YAxis stroke={reportTheme.text} fontSize={9} tickLine={false} domain={[0, 100]} />
<Area type="monotone" dataKey="humidity" stroke={reportTheme.humidity} fill={reportTheme.humidity} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="h-48 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">VPD History (kPa)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={9} tick={false} />
<YAxis stroke={reportTheme.text} fontSize={9} tickLine={false} domain={[0, 2.5]} />
<Area type="monotone" dataKey="vpd" stroke={reportTheme.vpd} fill={reportTheme.vpd} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
{/* Data Tables Section */}
<div className="mb-8">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Alert & Intervention Analysis
</h2>
{/* KPI Table */}
<table className="w-full text-sm text-left border border-slate-200 mb-6">
<thead className="bg-slate-100 text-slate-700 font-semibold uppercase text-xs">
<tr>
<th className="px-4 py-2">Time</th>
<th className="px-4 py-2">Severity</th>
<th className="px-4 py-2">Alert Type</th>
<th className="px-4 py-2">Message</th>
<th className="px-4 py-2 text-right">Duration</th>
<th className="px-4 py-3 border-b border-slate-200">Metric</th>
<th className="px-4 py-3 border-b border-slate-200">Value</th>
<th className="px-4 py-3 border-b border-slate-200">Notes</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{reportData.alertAnalytics.recentAlerts.slice(0, 15).map((alert, idx) => (
<tr key={alert.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
<td className="px-4 py-2 whitespace-nowrap text-slate-600">
{new Date(alert.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' })}
</td>
<td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full font-bold text-[10px] ${alert.severity === 'CRITICAL' ? 'bg-red-100 text-red-800' :
alert.severity === 'WARNING' ? 'bg-amber-100 text-amber-800' :
'bg-blue-100 text-blue-800'
}`}>
{alert.severity}
</span>
</td>
<td className="px-4 py-2 font-medium text-slate-900">{alert.type.replace('_', ' ')}</td>
<td className="px-4 py-2 text-slate-600 truncate max-w-xs">{alert.message}</td>
<td className="px-4 py-2 text-right font-mono text-slate-700">
{alert.resolutionTimeMin ? `${alert.resolutionTimeMin.toFixed(1)}m` : '-'}
</td>
</tr>
))}
<tr>
<td className="px-4 py-2 font-medium">Avg Resolution Time</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.resolutionTimes.avgMinutes || '--'} min</td>
<td className="px-4 py-2 text-slate-500">Average time to return to normal range</td>
</tr>
<tr>
<td className="px-4 py-2 font-medium">Resolution Rate</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.summary.resolutionRate}%</td>
<td className="px-4 py-2 text-slate-500">{reportData?.alertAnalytics?.summary.resolved} resolved out of {reportData?.alertAnalytics?.summary.total} total</td>
</tr>
<tr>
<td className="px-4 py-2 font-medium">Slowest Response</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.resolutionTimes.maxMinutes || '--'} min</td>
<td className="px-4 py-2 text-slate-500">Max deviation duration</td>
</tr>
</tbody>
</table>
) : (
<p className="text-slate-500 italic text-sm p-4 border border-slate-200 rounded">No recent alerts found for this period.</p>
)}
</div>
{/* Footer */}
<div className="mt-12 pt-6 border-t border-slate-200 text-center text-xs text-slate-400">
<p>© {new Date().getFullYear()} Veridian Systems Generated via Environment Operations Manager</p>
{/* Recent Alerts List */}
{reportData?.alertAnalytics?.recentAlerts && reportData.alertAnalytics.recentAlerts.length > 0 ? (
<table className="w-full text-xs text-left border border-slate-200">
<thead className="bg-slate-800 text-white font-semibold uppercase">
<tr>
<th className="px-4 py-2">Time</th>
<th className="px-4 py-2">Severity</th>
<th className="px-4 py-2">Alert Type</th>
<th className="px-4 py-2">Message</th>
<th className="px-4 py-2 text-right">Duration</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{reportData.alertAnalytics.recentAlerts.slice(0, 15).map((alert, idx) => (
<tr key={alert.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
<td className="px-4 py-2 whitespace-nowrap text-slate-600">
{new Date(alert.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' })}
</td>
<td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full font-bold text-[10px] ${alert.severity === 'CRITICAL' ? 'bg-red-100 text-red-800' :
alert.severity === 'WARNING' ? 'bg-amber-100 text-amber-800' :
'bg-blue-100 text-blue-800'
}`}>
{alert.severity}
</span>
</td>
<td className="px-4 py-2 font-medium text-slate-900">{alert.type.replace('_', ' ')}</td>
<td className="px-4 py-2 text-slate-600 truncate max-w-xs">{alert.message}</td>
<td className="px-4 py-2 text-right font-mono text-slate-700">
{alert.resolutionTimeMin ? `${alert.resolutionTimeMin.toFixed(1)}m` : '-'}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-slate-500 italic text-sm p-4 border border-slate-200 rounded">No recent alerts found for this period.</p>
)}
</div>
{/* Footer */}
<div className="mt-12 pt-6 border-t border-slate-200 text-center text-xs text-slate-400">
<p>© {new Date().getFullYear()} Veridian Systems Generated via Environment Operations Manager</p>
</div>
</div>
</div>
</div>