From fc3f2ac36859ab35dd245cc0572eaeb3302d1cf2 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Tue, 2 Sep 2025 08:18:55 -0500 Subject: [PATCH] push weather --- .env | 37 +++++++++ .../add_unique_constraint_weather_data.sql | 18 +++++ .../Applications/ApplicationViewModal.js | 40 +++++++++- frontend/src/pages/Weather/Weather.js | 79 +++++++++++++++++-- 4 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 .env create mode 100644 database/migrations/add_unique_constraint_weather_data.sql diff --git a/.env b/.env new file mode 100644 index 0000000..9d931f4 --- /dev/null +++ b/.env @@ -0,0 +1,37 @@ +# TurfTracker Environment Configuration +# Copy this file to .env and fill in your values + +# Database Configuration +# You can use either individual fields OR a full DATABASE_URL (DATABASE_URL takes precedence) +DB_HOST=db +DB_PORT=5432 +DB_NAME=turftracker +DB_USER=turftracker +DB_PASSWORD=password123 +# DATABASE_URL=postgresql://turftracker:password123@db:5432/turftracker + +# JWT Secret - REQUIRED: Used for signing authentication tokens +# Generate a secure random string (see README for generation commands) +# NEVER use the default value in production! +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production + +# Authentik OAuth2 Configuration (Optional) +# Configure these to enable SSO login through your Authentik instance +AUTHENTIK_CLIENT_ID=your-authentik-client-id +AUTHENTIK_CLIENT_SECRET=your-authentik-client-secret +AUTHENTIK_BASE_URL=https://your-authentik-domain.com +AUTHENTIK_CALLBACK_URL=https://turftracker.kaspers.us/api/auth/authentik/callback + +# Weather API Configuration +# Get a free API key from https://openweathermap.org/api +WEATHER_API_KEY=5bae1b310158565c65f982d4074e803b + +# Application URLs (automatically configured for production) +FRONTEND_URL=https://turftracker.kaspers.us + +# Node Environment +NODE_ENV=development + +# Port Configuration (optional - defaults are set) +PORT=5000 +FRONTEND_PORT=3000 \ No newline at end of file diff --git a/database/migrations/add_unique_constraint_weather_data.sql b/database/migrations/add_unique_constraint_weather_data.sql new file mode 100644 index 0000000..0a9947c --- /dev/null +++ b/database/migrations/add_unique_constraint_weather_data.sql @@ -0,0 +1,18 @@ +-- Ensure weather_data upsert works by providing a unique index +-- Safe to run multiple times +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_indexes + WHERE schemaname = 'public' AND indexname = 'idx_weather_data_property_date' + ) THEN + DROP INDEX IF EXISTS idx_weather_data_property_date; + END IF; +EXCEPTION WHEN undefined_table THEN + -- table may not exist yet in some environments; ignore + NULL; +END $$; + +CREATE UNIQUE INDEX IF NOT EXISTS ux_weather_data_property_date + ON weather_data(property_id, date); + diff --git a/frontend/src/components/Applications/ApplicationViewModal.js b/frontend/src/components/Applications/ApplicationViewModal.js index 545fc86..b5ad223 100644 --- a/frontend/src/components/Applications/ApplicationViewModal.js +++ b/frontend/src/components/Applications/ApplicationViewModal.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import * as turf from '@turf/turf'; -import { applicationsAPI } from '../../services/api'; +import { applicationsAPI, weatherAPI } from '../../services/api'; import PropertyMap from '../Maps/PropertyMap'; import { XMarkIcon, ClockIcon, MapPinIcon, WrenchScrewdriverIcon, BeakerIcon } from '@heroicons/react/24/outline'; @@ -9,6 +9,8 @@ const ApplicationViewModal = ({ application, propertyDetails, onClose }) => { const [sections, setSections] = useState([]); const [planDetails, setPlanDetails] = useState(null); const [loading, setLoading] = useState(true); + const [weather, setWeather] = useState(null); + const [weatherError, setWeatherError] = useState(null); // Haversine distance between two lat/lng in meters const haversineMeters = (lat1, lng1, lat2, lng2) => { @@ -135,6 +137,19 @@ const ApplicationViewModal = ({ application, propertyDetails, onClose }) => { console.log('No application logs found:', error); } + // Fetch current weather for this plan's property + try { + const propId = fetchedPlanDetails.property_id || application.propertyId; + if (propId) { + const wx = await weatherAPI.getCurrent(propId); + setWeather(wx.data.data.weather); + setWeatherError(null); + } + } catch (e) { + console.warn('Weather fetch failed:', e?.response?.data || e.message); + setWeatherError('Weather unavailable'); + } + } catch (error) { console.error('Failed to fetch application data:', error); } finally { @@ -242,6 +257,29 @@ const ApplicationViewModal = ({ application, propertyDetails, onClose }) => { + {/* Weather */} +
+
+

Current Weather

+ {weather ? ( +
+
+ icon +
{weather.current.temperature}°F
+
+
{weather.current.conditions}
+
Humidity: {weather.current.humidity}%
+
Wind: {Math.round(weather.current.windSpeed)} mph
+
+ ) : weatherError ? ( +
{weatherError}
+ ) : ( +
Loading weather…
+ )} +
+ +
+ {/* Products */} {planDetails?.products && planDetails.products.length > 0 && (
diff --git a/frontend/src/pages/Weather/Weather.js b/frontend/src/pages/Weather/Weather.js index f9d7dad..9621788 100644 --- a/frontend/src/pages/Weather/Weather.js +++ b/frontend/src/pages/Weather/Weather.js @@ -1,14 +1,83 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { propertiesAPI, weatherAPI } from '../../services/api'; const Weather = () => { + const [properties, setProperties] = useState([]); + const [weatherMap, setWeatherMap] = useState({}); // propertyId -> weather + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const load = async () => { + try { + setLoading(true); + const resp = await propertiesAPI.getAll(); + const props = resp.data?.data?.properties || []; + setProperties(props); + + // Fetch current weather for each property with coordinates + const ids = props + .filter(p => p.latitude && p.longitude) + .map(p => p.id); + + const results = await Promise.allSettled(ids.map(id => weatherAPI.getCurrent(id))); + const map = {}; + results.forEach((r, idx) => { + const id = ids[idx]; + if (r.status === 'fulfilled') { + map[id] = r.value.data.data.weather; + } + }); + setWeatherMap(map); + setError(null); + } catch (e) { + console.error('Failed to load weather:', e); + setError('Failed to load weather'); + } finally { + setLoading(false); + } + }; + load(); + }, []); + return (

Weather

-
-

Weather information coming soon...

-
+ + {loading ? ( +
Loading…
+ ) : error ? ( +
{error}
+ ) : ( +
+ {properties.map(p => ( +
+
+
{p.name}
+
{p.address}
+
+ {!p.latitude || !p.longitude ? ( +
No coordinates set
+ ) : weatherMap[p.id] ? ( +
+ icon +
+
{weatherMap[p.id].current.temperature}°F
+
{weatherMap[p.id].current.conditions}
+
+ Humidity {weatherMap[p.id].current.humidity}% • Wind {Math.round(weatherMap[p.id].current.windSpeed)} mph +
+
+
+ ) : ( +
Loading weather…
+ )} +
+ ))} +
+ )}
); }; -export default Weather; \ No newline at end of file +export default Weather;