fix: graceful fallback for network graph API errors + Map fixes

Network Graph:
- Add graceful fallback when API returns 401 or other errors
- Falls back to showing room participants as nodes
- Prevents error spam in console for unauthenticated users

Map Shape (linter changes):
- Add isFetchingNearby state for loading indicator
- Improve addAnnotation to accept name/color options
- Add logging for Find Nearby debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-07 16:47:19 -08:00
parent 34d7fd71a6
commit 27c82246ef
2 changed files with 66 additions and 17 deletions

View File

@ -156,10 +156,36 @@ export function useNetworkGraph(options: UseNetworkGraphOptions = {}): UseNetwor
// Fetch graph, optionally scoped to room // Fetch graph, optionally scoped to room
let graph: NetworkGraph; let graph: NetworkGraph;
if (participantIds.length > 0) { try {
graph = await getRoomNetworkGraph(participantIds); if (participantIds.length > 0) {
} else { graph = await getRoomNetworkGraph(participantIds);
graph = await getMyNetworkGraph(); } else {
graph = await getMyNetworkGraph();
}
} catch (apiError: any) {
// If API call fails (e.g., 401 Unauthorized), fall back to showing room participants
console.warn('Network graph API failed, falling back to room participants:', apiError.message);
const fallbackNodes: GraphNode[] = roomParticipants.map(participant => ({
id: participant.id,
username: participant.username,
displayName: participant.username,
avatarColor: participant.color,
isInRoom: true,
roomPresenceColor: participant.color,
isCurrentUser: participant.id === session.username || participant.id === roomParticipants[0]?.id,
isAnonymous: false,
trustLevelTo: undefined,
trustLevelFrom: undefined,
}));
setState({
nodes: fallbackNodes,
edges: [],
myConnections: [],
isLoading: false,
error: null, // Don't show error to user - graceful degradation
});
return;
} }
// Enrich nodes with room status, current user flag, and anonymous status // Enrich nodes with room status, current user flag, and anonymous status

View File

@ -338,6 +338,7 @@ function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor:
const [searchResults, setSearchResults] = useState<any[]>([]); const [searchResults, setSearchResults] = useState<any[]>([]);
const [_nearbyPlaces, setNearbyPlaces] = useState<any[]>([]); const [_nearbyPlaces, setNearbyPlaces] = useState<any[]>([]);
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [isFetchingNearby, setIsFetchingNearby] = useState(false);
const [observingUser, setObservingUser] = useState<string | null>(null); const [observingUser, setObservingUser] = useState<string | null>(null);
const styleKey = (shape.props.styleKey || 'voyager') as StyleKey; const styleKey = (shape.props.styleKey || 'voyager') as StyleKey;
@ -578,12 +579,12 @@ function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor:
// Actions // Actions
// ========================================================================== // ==========================================================================
const addAnnotation = useCallback((type: Annotation['type'], coordinates: Coordinate[]) => { const addAnnotation = useCallback((type: Annotation['type'], coordinates: Coordinate[], options?: { name?: string; color?: string }) => {
const newAnnotation: Annotation = { const newAnnotation: Annotation = {
id: `ann-${Date.now()}`, id: `ann-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
type, type,
name: `${type.charAt(0).toUpperCase() + type.slice(1)} ${shape.props.annotations.length + 1}`, name: options?.name || `${type.charAt(0).toUpperCase() + type.slice(1)} ${shape.props.annotations.length + 1}`,
color: selectedColor, color: options?.color || selectedColor,
visible: true, visible: true,
coordinates, coordinates,
createdAt: Date.now(), createdAt: Date.now(),
@ -680,11 +681,16 @@ function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor:
const findNearby = useCallback(async (category: typeof NEARBY_CATEGORIES[0]) => { const findNearby = useCallback(async (category: typeof NEARBY_CATEGORIES[0]) => {
if (!mapRef.current || !isMountedRef.current) return; if (!mapRef.current || !isMountedRef.current) return;
console.log('🗺️ findNearby called for category:', category.label);
setIsFetchingNearby(true);
let bounds; let bounds;
try { try {
bounds = mapRef.current.getBounds(); bounds = mapRef.current.getBounds();
console.log('🗺️ Map bounds:', bounds.toString());
} catch (err) { } catch (err) {
// Map may have been destroyed console.error('🗺️ Error getting bounds:', err);
setIsFetchingNearby(false);
return; return;
} }
@ -696,15 +702,21 @@ function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor:
); );
out body 10; out body 10;
`; `;
console.log('🗺️ Overpass query:', query);
const response = await fetch('https://overpass-api.de/api/interpreter', { const response = await fetch('https://overpass-api.de/api/interpreter', {
method: 'POST', method: 'POST',
body: query, body: query,
}); });
if (!isMountedRef.current) return; if (!isMountedRef.current) {
setIsFetchingNearby(false);
return;
}
console.log('🗺️ Overpass response status:', response.status);
const data = await response.json() as { elements: { id: number; lat: number; lon: number; tags?: { name?: string; amenity?: string } }[] }; const data = await response.json() as { elements: { id: number; lat: number; lon: number; tags?: { name?: string; amenity?: string } }[] };
console.log('🗺️ Found', data.elements.length, 'places');
const places = data.elements.slice(0, 10).map((el) => ({ const places = data.elements.slice(0, 10).map((el) => ({
id: el.id, id: el.id,
@ -715,17 +727,26 @@ function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor:
color: category.color, color: category.color,
})); }));
if (!isMountedRef.current) return; if (!isMountedRef.current) {
setIsFetchingNearby(false);
return;
}
setNearbyPlaces(places); setNearbyPlaces(places);
// Add markers for nearby places // Add markers for nearby places
console.log('🗺️ Adding', places.length, 'markers');
places.forEach((place: any) => { places.forEach((place: any) => {
if (isMountedRef.current) { if (isMountedRef.current) {
addAnnotation('marker', [{ lat: place.lat, lng: place.lng }]); addAnnotation('marker', [{ lat: place.lat, lng: place.lng }], {
name: place.name,
color: place.color,
});
} }
}); });
setIsFetchingNearby(false);
} catch (err) { } catch (err) {
console.error('Find nearby error:', err); console.error('🗺️ Find nearby error:', err);
setIsFetchingNearby(false);
} }
}, [addAnnotation]); }, [addAnnotation]);
@ -972,19 +993,21 @@ function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor:
{/* Find Nearby */} {/* Find Nearby */}
<div style={styles.section}> <div style={styles.section}>
<div style={styles.sectionTitle}>Find nearby</div> <div style={styles.sectionTitle}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 4 }}> Find nearby {isFetchingNearby && <span style={{ marginLeft: 8, fontSize: 12 }}></span>}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 4, opacity: isFetchingNearby ? 0.5 : 1 }}>
{NEARBY_CATEGORIES.map((cat) => ( {NEARBY_CATEGORIES.map((cat) => (
<div <div
key={cat.key} key={cat.key}
className="mapus-category" className="mapus-category"
onClick={() => findNearby(cat)} onClick={() => !isFetchingNearby && findNearby(cat)}
onPointerDown={stopPropagation} onPointerDown={stopPropagation}
style={{ style={{
textAlign: 'center', textAlign: 'center',
padding: '10px 4px', padding: '10px 4px',
borderRadius: 6, borderRadius: 6,
cursor: 'pointer', cursor: isFetchingNearby ? 'wait' : 'pointer',
fontSize: 11, fontSize: 11,
color: '#626C72', color: '#626C72',
}} }}