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>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue