diff --git a/songbird-dashboard/src/api/alerts.test.ts b/songbird-dashboard/src/api/alerts.test.ts index 88fa180..325f309 100644 --- a/songbird-dashboard/src/api/alerts.test.ts +++ b/songbird-dashboard/src/api/alerts.test.ts @@ -14,31 +14,33 @@ beforeEach(() => { }); describe('getAlerts', () => { - it('calls apiGet with /v1/alerts and no query string when no params', async () => { + it('calls apiGet with /v1/alerts and no params when called without args', async () => { await getAlerts(); - expect(apiGet).toHaveBeenCalledWith('/v1/alerts'); + expect(apiGet).toHaveBeenCalledWith('/v1/alerts', undefined); }); - it('builds query string with serial_number', async () => { + it('passes serial_number as a query param', async () => { await getAlerts({ serial_number: 'sb01' }); - expect(apiGet).toHaveBeenCalledWith('/v1/alerts?serial_number=sb01'); + expect(apiGet).toHaveBeenCalledWith('/v1/alerts', { serial_number: 'sb01' }); }); - it('builds query string with acknowledged flag', async () => { + it('passes acknowledged flag as a query param', async () => { await getAlerts({ acknowledged: false }); - expect(apiGet).toHaveBeenCalledWith('/v1/alerts?acknowledged=false'); + expect(apiGet).toHaveBeenCalledWith('/v1/alerts', { acknowledged: false }); }); - it('builds query string with limit', async () => { + it('passes limit as a query param', async () => { await getAlerts({ limit: 50 }); - expect(apiGet).toHaveBeenCalledWith('/v1/alerts?limit=50'); + expect(apiGet).toHaveBeenCalledWith('/v1/alerts', { limit: 50 }); }); - it('builds query string with all params combined', async () => { + it('passes all params combined', async () => { await getAlerts({ serial_number: 'sb01', acknowledged: true, limit: 10 }); - expect(apiGet).toHaveBeenCalledWith( - '/v1/alerts?serial_number=sb01&acknowledged=true&limit=10' - ); + expect(apiGet).toHaveBeenCalledWith('/v1/alerts', { + serial_number: 'sb01', + acknowledged: true, + limit: 10, + }); }); }); diff --git a/songbird-dashboard/src/api/alerts.ts b/songbird-dashboard/src/api/alerts.ts index e5ca114..deb8365 100644 --- a/songbird-dashboard/src/api/alerts.ts +++ b/songbird-dashboard/src/api/alerts.ts @@ -19,13 +19,7 @@ export async function getAlerts(params?: { acknowledged?: boolean; limit?: number; }): Promise { - const searchParams = new URLSearchParams(); - if (params?.serial_number) searchParams.set('serial_number', params.serial_number); - if (params?.acknowledged !== undefined) searchParams.set('acknowledged', String(params.acknowledged)); - if (params?.limit) searchParams.set('limit', String(params.limit)); - - const query = searchParams.toString(); - return apiGet(`/v1/alerts${query ? `?${query}` : ''}`); + return apiGet('/v1/alerts', params as Record); } /** diff --git a/songbird-dashboard/src/components/layout/MobileNav.tsx b/songbird-dashboard/src/components/layout/MobileNav.tsx index 71a25a1..40fd17e 100644 --- a/songbird-dashboard/src/components/layout/MobileNav.tsx +++ b/songbird-dashboard/src/components/layout/MobileNav.tsx @@ -1,7 +1,6 @@ import { useState, useMemo } from 'react'; import { NavLink } from 'react-router-dom'; import { Menu } from 'lucide-react'; -import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Sheet, @@ -9,40 +8,15 @@ import { SheetHeader, SheetTitle, } from '@/components/ui/sheet'; -import { - LayoutDashboard, - Cpu, - AlertTriangle, - Settings, - Map, - Terminal, - Sparkles, -} from 'lucide-react'; -import { useFeatureFlags, type FeatureFlagKey } from '@/hooks/useFeatureFlags'; - -interface NavItem { - to: string; - icon: typeof LayoutDashboard; - label: string; - featureFlag?: FeatureFlagKey; -} - -const navItems: NavItem[] = [ - { to: '/', icon: LayoutDashboard, label: 'Dashboard' }, - { to: '/devices', icon: Cpu, label: 'Devices' }, - { to: '/map', icon: Map, label: 'Fleet Map' }, - { to: '/alerts', icon: AlertTriangle, label: 'Alerts' }, - { to: '/commands', icon: Terminal, label: 'Commands' }, - { to: '/analytics', icon: Sparkles, label: 'Analytics', featureFlag: 'analytics' }, - { to: '/settings', icon: Settings, label: 'Settings' }, -]; +import { useFeatureFlags } from '@/hooks/useFeatureFlags'; +import { NAV_ITEMS, navLinkClass } from '@/config/navigation'; export function MobileNav() { const [open, setOpen] = useState(false); const flags = useFeatureFlags(); const visibleNavItems = useMemo(() => { - return navItems.filter(item => { + return NAV_ITEMS.filter(item => { if (!item.featureFlag) return true; return flags[item.featureFlag]; }); @@ -72,14 +46,7 @@ export function MobileNav() { key={item.to} to={item.to} onClick={() => setOpen(false)} - className={({ isActive }) => - cn( - 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors', - isActive - ? 'bg-primary text-primary-foreground' - : 'text-muted-foreground hover:bg-muted hover:text-foreground' - ) - } + className={({ isActive }) => navLinkClass(isActive)} > {item.label} diff --git a/songbird-dashboard/src/components/maps/FleetMap.tsx b/songbird-dashboard/src/components/maps/FleetMap.tsx index 28e6b27..1a9cf80 100644 --- a/songbird-dashboard/src/components/maps/FleetMap.tsx +++ b/songbird-dashboard/src/components/maps/FleetMap.tsx @@ -1,37 +1,15 @@ import { useEffect, useRef, useState, useMemo, useCallback } from 'react'; import Map, { Marker, Popup, NavigationControl } from 'react-map-gl'; import type { MapRef } from 'react-map-gl'; -import { MapPin, Satellite, Radio } from 'lucide-react'; +import { MapPin } from 'lucide-react'; import { DeviceStatus } from '@/components/devices/DeviceStatus'; import { formatRelativeTime } from '@/utils/formatters'; +import { getLocationSourceInfo } from '@/utils/locationSource'; +import { MAP_STYLES, DEFAULT_MAP_CENTER } from '@/config/mapConfig'; import { usePreferences } from '@/contexts/PreferencesContext'; -import type { Device, LocationSource } from '@/types'; +import type { Device } from '@/types'; import 'mapbox-gl/dist/mapbox-gl.css'; -// Location source display configuration -function getLocationSourceInfo(source?: LocationSource | string) { - switch (source) { - case 'gps': - return { label: 'GPS', icon: Satellite, color: 'text-green-600' }; - case 'cell': - case 'tower': - return { label: 'Cell Tower', icon: Radio, color: 'text-blue-600' }; - case 'wifi': - return { label: 'Wi-Fi', icon: Radio, color: 'text-purple-600' }; - case 'triangulation': - case 'triangulated': - return { label: 'Triangulation', icon: Radio, color: 'text-orange-600' }; - default: - return null; - } -} - -// Map style URLs -const MAP_STYLES = { - street: 'mapbox://styles/mapbox/light-v11', - satellite: 'mapbox://styles/mapbox/satellite-streets-v12', -}; - interface FleetMapProps { devices: Device[]; mapboxToken: string; @@ -111,15 +89,12 @@ export function FleetMap({ // No longer auto-show popup for selected device - hover handles it - // Default center (Austin, TX) - const defaultCenter = { longitude: -97.7431, latitude: 30.2672 }; - return (
{(() => { const sourceInfo = getLocationSourceInfo(hoveredDevice.location_source); - if (!sourceInfo) return null; const SourceIcon = sourceInfo.icon; return (
diff --git a/songbird-dashboard/src/components/maps/LocationTrail.tsx b/songbird-dashboard/src/components/maps/LocationTrail.tsx index 8e2bad2..72536fb 100644 --- a/songbird-dashboard/src/components/maps/LocationTrail.tsx +++ b/songbird-dashboard/src/components/maps/LocationTrail.tsx @@ -2,16 +2,11 @@ import { useMemo, useRef, useEffect, useState, useCallback } from 'react'; import Map, { Source, Layer, Marker, NavigationControl } from 'react-map-gl'; import type { MapRef } from 'react-map-gl'; import { MapPin } from 'lucide-react'; +import { MAP_STYLES, DEFAULT_MAP_CENTER } from '@/config/mapConfig'; import { usePreferences } from '@/contexts/PreferencesContext'; import type { LocationPoint } from '@/types'; import 'mapbox-gl/dist/mapbox-gl.css'; -// Map style URLs -const MAP_STYLES = { - street: 'mapbox://styles/mapbox/light-v11', - satellite: 'mapbox://styles/mapbox/satellite-streets-v12', -}; - interface LocationTrailProps { locations: LocationPoint[]; currentLocation?: { lat: number; lon: number }; @@ -116,15 +111,12 @@ export function LocationTrail({ const currentLocation = locations[0] || (deviceLocation ? { lat: deviceLocation.lat, lon: deviceLocation.lon, time: '' } : null); const trailPoints = locations.slice(1); - // Default center (Austin, TX) - const defaultCenter = { latitude: 30.2672, longitude: -97.7431 }; - return (
Math.round((c * 9) / 5 + 32); -const fahrenheitToCelsius = (f: number) => Math.round(((f - 32) * 5) / 9); - export function FleetDefaults() { const { data: fleets, isLoading: fleetsLoading } = useNotehubFleets(); const [selectedFleet, setSelectedFleet] = useState(''); @@ -38,7 +35,8 @@ export function FleetDefaults() { const tempUnit = useFahrenheit ? '°F' : '°C'; // Convert display temperature based on preference - const displayTemp = (celsius: number) => useFahrenheit ? celsiusToFahrenheit(celsius) : celsius; + // Convert display temperature based on preference (rounded for slider display) + const displayTemp = (celsius: number) => useFahrenheit ? Math.round(celsiusToFahrenheit(celsius)) : celsius; // Slider ranges based on unit const tempHighMin = useFahrenheit ? 14 : -10; // -10°C = 14°F @@ -233,7 +231,7 @@ export function FleetDefaults() { { - const celsius = useFahrenheit ? fahrenheitToCelsius(v) : v; + const celsius = useFahrenheit ? Math.round(fahrenheitToCelsius(v)) : v; updateLocalConfig('temp_alert_high_c', celsius); }} min={tempHighMin} @@ -252,7 +250,7 @@ export function FleetDefaults() { { - const celsius = useFahrenheit ? fahrenheitToCelsius(v) : v; + const celsius = useFahrenheit ? Math.round(fahrenheitToCelsius(v)) : v; updateLocalConfig('temp_alert_low_c', celsius); }} min={tempLowMin} diff --git a/songbird-dashboard/src/config/mapConfig.ts b/songbird-dashboard/src/config/mapConfig.ts new file mode 100644 index 0000000..f931fbb --- /dev/null +++ b/songbird-dashboard/src/config/mapConfig.ts @@ -0,0 +1,10 @@ +/** + * Shared map configuration constants + */ + +export const MAP_STYLES = { + street: 'mapbox://styles/mapbox/light-v11', + satellite: 'mapbox://styles/mapbox/satellite-streets-v12', +} as const; + +export const DEFAULT_MAP_CENTER = { longitude: -97.7431, latitude: 30.2672 } as const; diff --git a/songbird-dashboard/src/config/preferences.ts b/songbird-dashboard/src/config/preferences.ts new file mode 100644 index 0000000..0bac897 --- /dev/null +++ b/songbird-dashboard/src/config/preferences.ts @@ -0,0 +1,13 @@ +/** + * Shared display preferences defaults + */ + +import type { DisplayPreferences } from '@/types'; + +export const DEFAULT_PREFERENCES: DisplayPreferences = { + temp_unit: 'celsius', + time_format: '24h', + default_time_range: '24', + map_style: 'street', + distance_unit: 'km', +}; diff --git a/songbird-dashboard/src/contexts/PreferencesContext.tsx b/songbird-dashboard/src/contexts/PreferencesContext.tsx index d149ac8..1b30145 100644 --- a/songbird-dashboard/src/contexts/PreferencesContext.tsx +++ b/songbird-dashboard/src/contexts/PreferencesContext.tsx @@ -6,16 +6,9 @@ import { createContext, useContext, ReactNode } from 'react'; import { useUserProfile } from '@/hooks/useUserProfile'; +import { DEFAULT_PREFERENCES } from '@/config/preferences'; import type { DisplayPreferences } from '@/types'; -const DEFAULT_PREFERENCES: DisplayPreferences = { - temp_unit: 'celsius', - time_format: '24h', - default_time_range: '24', - map_style: 'street', - distance_unit: 'km', -}; - interface PreferencesContextValue { preferences: DisplayPreferences; isLoading: boolean; diff --git a/songbird-dashboard/src/hooks/useUserProfile.ts b/songbird-dashboard/src/hooks/useUserProfile.ts index 2e097c2..e49b52d 100644 --- a/songbird-dashboard/src/hooks/useUserProfile.ts +++ b/songbird-dashboard/src/hooks/useUserProfile.ts @@ -1,5 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { fetchUserAttributes, updateUserAttribute, updateUserAttributes } from 'aws-amplify/auth'; +import { DEFAULT_PREFERENCES } from '@/config/preferences'; import type { DisplayPreferences } from '@/types'; export interface UserProfile { @@ -11,14 +12,6 @@ export interface UserProfile { preferences: DisplayPreferences; } -const DEFAULT_PREFERENCES: DisplayPreferences = { - temp_unit: 'celsius', - time_format: '24h', - default_time_range: '24', - map_style: 'street', - distance_unit: 'km', -}; - async function getUserProfile(): Promise { const attributes = await fetchUserAttributes(); return { diff --git a/songbird-dashboard/src/pages/DeviceDetail.tsx b/songbird-dashboard/src/pages/DeviceDetail.tsx index 3924a04..eb25332 100644 --- a/songbird-dashboard/src/pages/DeviceDetail.tsx +++ b/songbird-dashboard/src/pages/DeviceDetail.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useParams, Link, useSearchParams } from 'react-router-dom'; -import { ArrowLeft, ArrowRight, Settings, Thermometer, Droplets, Gauge, Battery, BatteryFull, BatteryCharging, Zap, AlertTriangle, Check, CheckCheck, Clock, Activity, MapPin, Lock, Route, Navigation, ExternalLink } from 'lucide-react'; +import { ArrowLeft, ArrowRight, Settings, Thermometer, Droplets, Gauge, Battery, BatteryFull, BatteryCharging, Zap, AlertTriangle, Check, CheckCheck, Clock, Activity, MapPin, Satellite, Lock, Route, Navigation, ExternalLink } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; diff --git a/songbird-dashboard/src/utils/formatters.ts b/songbird-dashboard/src/utils/formatters.ts index 6ee9664..e3d4e44 100644 --- a/songbird-dashboard/src/utils/formatters.ts +++ b/songbird-dashboard/src/utils/formatters.ts @@ -43,6 +43,13 @@ export function celsiusToFahrenheit(celsius: number): number { return (celsius * 9) / 5 + 32; } +/** + * Convert Fahrenheit to Celsius + */ +export function fahrenheitToCelsius(fahrenheit: number): number { + return ((fahrenheit - 32) * 5) / 9; +} + /** * Format temperature with unit * @param value - Temperature in Celsius diff --git a/songbird-dashboard/src/utils/locationSource.ts b/songbird-dashboard/src/utils/locationSource.ts new file mode 100644 index 0000000..8492461 --- /dev/null +++ b/songbird-dashboard/src/utils/locationSource.ts @@ -0,0 +1,29 @@ +/** + * Shared location source display helpers + */ + +import { Satellite, Radio, MapPin } from 'lucide-react'; + +export interface LocationSourceInfo { + label: string; + icon: typeof MapPin; + color: string; + bgColor: string; +} + +export function getLocationSourceInfo(source?: string): LocationSourceInfo { + switch (source) { + case 'gps': + return { label: 'GPS', icon: Satellite, color: 'text-green-600', bgColor: 'bg-green-100' }; + case 'cell': + case 'tower': + return { label: 'Cell Tower', icon: Radio, color: 'text-blue-600', bgColor: 'bg-blue-100' }; + case 'wifi': + return { label: 'Wi-Fi', icon: Radio, color: 'text-purple-600', bgColor: 'bg-purple-100' }; + case 'triangulation': + case 'triangulated': + return { label: 'Triangulated', icon: Radio, color: 'text-orange-600', bgColor: 'bg-orange-100' }; + default: + return { label: 'Unknown', icon: MapPin, color: 'text-gray-600', bgColor: 'bg-gray-100' }; + } +}