Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions songbird-dashboard/src/api/alerts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});

Expand Down
8 changes: 1 addition & 7 deletions songbird-dashboard/src/api/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ export async function getAlerts(params?: {
acknowledged?: boolean;
limit?: number;
}): Promise<AlertsResponse> {
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<AlertsResponse>(`/v1/alerts${query ? `?${query}` : ''}`);
return apiGet<AlertsResponse>('/v1/alerts', params as Record<string, string | number | boolean>);
}

/**
Expand Down
41 changes: 4 additions & 37 deletions songbird-dashboard/src/components/layout/MobileNav.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,22 @@
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,
SheetContent,
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];
});
Expand Down Expand Up @@ -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.icon className="h-5 w-5" />
{item.label}
Expand Down
36 changes: 5 additions & 31 deletions songbird-dashboard/src/components/maps/FleetMap.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 (
<div className={className}>
<Map
ref={mapRef}
initialViewState={{
...defaultCenter,
...DEFAULT_MAP_CENTER,
zoom: 4,
}}
style={{ width: '100%', height: '100%' }}
Expand Down Expand Up @@ -185,7 +160,6 @@ export function FleetMap({
</div>
{(() => {
const sourceInfo = getLocationSourceInfo(hoveredDevice.location_source);
if (!sourceInfo) return null;
const SourceIcon = sourceInfo.icon;
return (
<div className={`flex items-center gap-1.5 ${sourceInfo.color}`}>
Expand Down
12 changes: 2 additions & 10 deletions songbird-dashboard/src/components/maps/LocationTrail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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 (
<div className={className}>
<Map
ref={mapRef}
initialViewState={{
...defaultCenter,
...DEFAULT_MAP_CENTER,
zoom: 4,
}}
style={{ width: '100%', height: '100%' }}
Expand Down
12 changes: 2 additions & 10 deletions songbird-dashboard/src/components/maps/VisitedCitiesMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@ 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, Calendar, Hash } from 'lucide-react';
import { MAP_STYLES, DEFAULT_MAP_CENTER } from '@/config/mapConfig';
import { usePreferences } from '@/contexts/PreferencesContext';
import { formatDateTime } from '@/utils/formatters';
import type { VisitedCity } 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 VisitedCitiesMapProps {
cities: VisitedCity[];
mapboxToken: string;
Expand Down Expand Up @@ -98,15 +93,12 @@ export function VisitedCitiesMap({
return 'text-gray-500';
};

// Default center (Austin, TX)
const defaultCenter = { longitude: -97.7431, latitude: 30.2672 };

return (
<div className={className}>
<Map
ref={mapRef}
initialViewState={{
...defaultCenter,
...DEFAULT_MAP_CENTER,
zoom: 4,
}}
style={{ width: '100%', height: '100%' }}
Expand Down
12 changes: 5 additions & 7 deletions songbird-dashboard/src/components/settings/FleetDefaults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@ import {
} from '@/components/ui/select';
import { useNotehubFleets, useFleetDefaults, useUpdateFleetDefaults } from '@/hooks/useSettings';
import { usePreferences } from '@/contexts/PreferencesContext';
import { celsiusToFahrenheit, fahrenheitToCelsius } from '@/utils/formatters';
import type { FleetDefaults as FleetDefaultsType, OperatingMode, MotionSensitivity } from '@/types';

// Temperature conversion helpers
const celsiusToFahrenheit = (c: number) => 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<string>('');
Expand All @@ -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
Expand Down Expand Up @@ -233,7 +231,7 @@ export function FleetDefaults() {
<Slider
value={[displayTemp(localConfig.temp_alert_high_c || 35)]}
onValueChange={([v]) => {
const celsius = useFahrenheit ? fahrenheitToCelsius(v) : v;
const celsius = useFahrenheit ? Math.round(fahrenheitToCelsius(v)) : v;
updateLocalConfig('temp_alert_high_c', celsius);
}}
min={tempHighMin}
Expand All @@ -252,7 +250,7 @@ export function FleetDefaults() {
<Slider
value={[displayTemp(localConfig.temp_alert_low_c || 5)]}
onValueChange={([v]) => {
const celsius = useFahrenheit ? fahrenheitToCelsius(v) : v;
const celsius = useFahrenheit ? Math.round(fahrenheitToCelsius(v)) : v;
updateLocalConfig('temp_alert_low_c', celsius);
}}
min={tempLowMin}
Expand Down
10 changes: 10 additions & 0 deletions songbird-dashboard/src/config/mapConfig.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions songbird-dashboard/src/config/preferences.ts
Original file line number Diff line number Diff line change
@@ -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',
};
9 changes: 1 addition & 8 deletions songbird-dashboard/src/contexts/PreferencesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 1 addition & 8 deletions songbird-dashboard/src/hooks/useUserProfile.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<UserProfile> {
const attributes = await fetchUserAttributes();
return {
Expand Down
2 changes: 1 addition & 1 deletion songbird-dashboard/src/pages/DeviceDetail.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
7 changes: 7 additions & 0 deletions songbird-dashboard/src/utils/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading