From ef8b368c4c877039aecfb85f88855f4eb81fa1dc Mon Sep 17 00:00:00 2001 From: Geeth Gunnampalli Date: Mon, 1 Dec 2025 15:59:34 -0600 Subject: [PATCH] feat: Update disaster types and enhance settings page functionality - Expanded available disaster types in the map page to include wildfire, hurricane, tornado, volcano, and heatwave. - Refactored settings page to replace alert types with watched regions, allowing users to add and manage specific regions for alerts. - Implemented region search functionality to enhance user experience when adding watched regions. - Updated user alert preferences to accommodate the new watched regions feature, improving alert customization. --- client/app/dashboard/map/page.tsx | 2 +- client/app/dashboard/settings/page.tsx | 198 +++++++------ client/app/onboarding/page.tsx | 41 +-- ...f34ec_rename_regions_to_watched_regions.py | 38 +++ ...2264546da1d9_simplify_alert_preferences.py | 40 +++ server/db_utils/db.py | 7 +- server/requirements.txt | 1 + server/routers/alerts.py | 73 +++-- server/routers/data_feed.py | 33 ++- server/scripts/backfill_coordinates.py | 150 ++++++++++ server/services/alert_generator.py | 102 ++----- server/services/analysis.py | 178 +++++++++-- server/services/database_service.py | 278 +++++++++++++++++- server/services/geocoding_service.py | 81 +++++ server/services/population_estimator.py | 244 +++++++++++---- server/tasks.py | 30 +- 16 files changed, 1153 insertions(+), 343 deletions(-) create mode 100644 server/alembic/versions/12935d5f34ec_rename_regions_to_watched_regions.py create mode 100644 server/alembic/versions/2264546da1d9_simplify_alert_preferences.py create mode 100644 server/scripts/backfill_coordinates.py create mode 100644 server/services/geocoding_service.py diff --git a/client/app/dashboard/map/page.tsx b/client/app/dashboard/map/page.tsx index 10443b3..0ff595a 100644 --- a/client/app/dashboard/map/page.tsx +++ b/client/app/dashboard/map/page.tsx @@ -87,7 +87,7 @@ export default function MapPage() { ); }; - const availableDisasterTypes = ["earthquake", "flood", "fire", "storm", "tsunami", "other"]; + const availableDisasterTypes = ["earthquake", "flood", "wildfire", "hurricane", "tornado", "tsunami", "volcano", "heatwave"]; const hasActiveFilters = countryFilter !== "" || disasterTypeFilters.length > 0 || locationFilter !== "all" || severityFilter !== "all"; const availableCountries = useMemo(() => { diff --git a/client/app/dashboard/settings/page.tsx b/client/app/dashboard/settings/page.tsx index 877f1e2..2f8986e 100644 --- a/client/app/dashboard/settings/page.tsx +++ b/client/app/dashboard/settings/page.tsx @@ -9,7 +9,6 @@ import { Switch } from "@/components/ui/switch" import { Button } from "@/components/ui/button" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Skeleton } from "@/components/ui/skeleton" -import { Checkbox } from "@/components/ui/checkbox" import { Select, SelectContent, @@ -17,7 +16,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { Search, Check, AlertCircle, Loader2, RefreshCw, Trash2, MapPin, Calendar, User, Edit2, X } from "lucide-react" +import { Search, Check, AlertCircle, Loader2, RefreshCw, Trash2, MapPin, Calendar, User, Edit2, X, Plus } from "lucide-react" import { apiClient } from "@/lib/api-client" import { MapStyleSettings } from "@/components/map-style-settings" import { @@ -30,19 +29,29 @@ import { } from "@/components/ui/dialog" import { useRouter } from "next/navigation" +interface WatchedRegion { + name: string + lat: number + lng: number + bounds?: { + ne_lat: number + ne_lng: number + sw_lat: number + sw_lng: number + } + place_id?: string +} + export default function SettingsPage() { const { user, loading, refreshAuth } = useAuth() const router = useRouter() const [emailNotifications, setEmailNotifications] = useState(false) - const [twoFactor, setTwoFactor] = useState(false) - const [autoUpdates, setAutoUpdates] = useState(false) - const [shareUsageData, setShareUsageData] = useState(true) // Alert preferences const [minSeverity, setMinSeverity] = useState(3) - const [emailMinSeverity, setEmailMinSeverity] = useState(3) - const [alertTypes, setAlertTypes] = useState(['new_crisis', 'severity_change', 'update']) - const [regions, setRegions] = useState('') + const [watchedRegions, setWatchedRegions] = useState([]) + const [regionSearch, setRegionSearch] = useState('') + const [searchingRegion, setSearchingRegion] = useState(false) const [saving, setSaving] = useState(false) const [saveSuccess, setSaveSuccess] = useState(false) const [updatingLocation, setUpdatingLocation] = useState(false) @@ -58,9 +67,7 @@ export default function SettingsPage() { if (response.ok) { const data = await response.json() setMinSeverity(data.min_severity || 3) - setEmailMinSeverity(data.email_min_severity || 3) - setAlertTypes(data.alert_types || ['new_crisis', 'severity_change', 'update']) - setRegions(data.regions?.join(', ') || '') + setWatchedRegions(data.watched_regions || []) setEmailNotifications(data.email_enabled || false) } } catch (err) { @@ -84,9 +91,7 @@ export default function SettingsPage() { method: 'PUT', body: JSON.stringify({ min_severity: minSeverity, - email_min_severity: emailMinSeverity, - alert_types: alertTypes, - regions: regions ? regions.split(',').map(r => r.trim()).filter(r => r) : null, + watched_regions: watchedRegions.length > 0 ? watchedRegions : null, email_enabled: overrides?.email_enabled ?? emailNotifications, }), }) @@ -102,10 +107,31 @@ export default function SettingsPage() { } } - const toggleAlertType = (type: string) => { - setAlertTypes(prev => - prev.includes(type) ? prev.filter(t => t !== type) : [...prev, type] - ) + const searchAndAddRegion = async () => { + if (!regionSearch.trim()) return + + setSearchingRegion(true) + try { + const response = await apiClient(`/api/alerts/regions/search?query=${encodeURIComponent(regionSearch)}`) + if (response.ok) { + const region: WatchedRegion = await response.json() + if (!watchedRegions.some(r => r.place_id === region.place_id)) { + setWatchedRegions([...watchedRegions, region]) + } + setRegionSearch('') + } else { + alert('Region not found. Try a different search term.') + } + } catch (err) { + console.error('Failed to search region:', err) + alert('Failed to search region') + } finally { + setSearchingRegion(false) + } + } + + const removeRegion = (index: number) => { + setWatchedRegions(watchedRegions.filter((_, i) => i !== index)) } const updateLocation = () => { @@ -510,19 +536,19 @@ export default function SettingsPage() {
-

How notifications work

+

How alerts work

-

Location-based: You'll receive alerts for disasters within 100km of your location

-

Severity filter: Only alerts at or above your minimum severity will appear

-

Alert types: Choose which types of alerts you want to see

-

Regions: Optionally specify regions to receive alerts from anywhere

+

Location-based: Alerts for disasters within 100km of your location

+

Severity filter: Only alerts at or above your minimum severity

+

Custom regions: Optionally add regions to get alerts from anywhere

+

No location: Receive all global alerts

- +

- Only alerts at this severity level or higher will appear in your dashboard. Lower severity = more alerts. + Only disasters at this severity or higher will trigger alerts (both dashboard and email).

-
- - -

- Only emails will be sent for alerts at this severity level or higher. This is separate from dashboard alerts. +

+ +

+ Get alerts from these regions even if they're outside your 100km radius.

-
+ +
+ setRegionSearch(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + void searchAndAddRegion() + } + }} + className="text-sm" + /> + +
-
- -

Select which types of alerts you want to receive:

-
- {[ - { value: 'new_crisis', label: 'New Crisis', desc: 'When a new disaster is first detected' }, - { value: 'severity_change', label: 'Severity Changes', desc: 'When an existing disaster becomes more severe' }, - { value: 'update', label: 'Updates', desc: 'General updates about ongoing crises' } - ].map(type => ( -
- toggleAlertType(type.value)} - className="mt-1" - /> -
- -

{type.desc}

+ {watchedRegions.length > 0 && ( +
+ {watchedRegions.map((region, index) => ( +
+
+ + {region.name} +
+
-
- ))} -
- {alertTypes.length === 0 && ( -

- ⚠️ You must select at least one alert type -

+ ))} +
)}
-
- - setRegions(e.target.value)} - className="text-sm" - /> -

- Optional: Enter specific regions (comma-separated) to receive alerts from anywhere, even outside your 100km radius. - Leave empty to only receive alerts based on your location radius. -

-
-
- {alertTypes.length === 0 && ( -

- Please select at least one alert type to save -

- )}
diff --git a/client/app/onboarding/page.tsx b/client/app/onboarding/page.tsx index ef253ac..ca42a7a 100644 --- a/client/app/onboarding/page.tsx +++ b/client/app/onboarding/page.tsx @@ -6,7 +6,6 @@ import { MapPin, CheckCircle2, Loader2, Search, Globe, Bell, AlertCircle, Chevro import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; -import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { Select, @@ -40,7 +39,6 @@ function OnboardingPageContent() { // Alert preferences state const [minSeverity, setMinSeverity] = useState(3); - const [alertTypes, setAlertTypes] = useState(['new_crisis', 'severity_change', 'update']); const [savingAlerts, setSavingAlerts] = useState(false); const router = useRouter(); @@ -191,12 +189,6 @@ function OnboardingPageContent() { } }; - const toggleAlertType = (type: string) => { - setAlertTypes(prev => - prev.includes(type) ? prev.filter(t => t !== type) : [...prev, type] - ); - }; - const saveAlertPreferences = async () => { if (!user?.user_id) return; @@ -206,10 +198,7 @@ function OnboardingPageContent() { method: 'PUT', body: JSON.stringify({ min_severity: minSeverity, - email_min_severity: 3, - alert_types: alertTypes, - regions: null, - disaster_types: null, + watched_regions: null, email_enabled: true, }), }); @@ -448,36 +437,10 @@ function OnboardingPageContent() {

-
- -
- {[ - { value: 'new_crisis', label: 'New Crisis', desc: 'When a new disaster is detected' }, - { value: 'severity_change', label: 'Severity Changes', desc: 'When disaster severity increases' }, - { value: 'update', label: 'Updates', desc: 'General updates about ongoing crises' } - ].map(type => ( -
- toggleAlertType(type.value)} - className="mt-1" - /> -
- -

{type.desc}

-
-
- ))} -
-
-