Add better error bubble-up

This commit is contained in:
Jack Kingsman
2026-01-13 01:19:15 -08:00
parent 40b672d569
commit 760bd0f7d6
7 changed files with 97 additions and 36 deletions

View File

@@ -617,16 +617,45 @@ The app uses Sonner for toast notifications via a custom wrapper at `components/
```typescript
import { toast } from './components/ui/sonner';
// Success toast
toast.success('Operation completed', { description: 'Details here' });
// Success toast (use sparingly - only for significant/destructive actions)
toast.success('Channel deleted');
// Error toast (muted red styling for readability)
toast.error('Operation failed', { description: 'Error details' });
// Error toast with details
toast.error('Failed to send message', {
description: err instanceof Error ? err.message : 'Check radio connection',
});
```
Toasts are automatically shown for:
### Error Handling Pattern
All async operations that can fail should show error toasts. Keep console.error for debugging:
```typescript
try {
await api.someOperation();
} catch (err) {
console.error('Failed to do X:', err);
toast.error('Failed to do X', {
description: err instanceof Error ? err.message : 'Check radio connection',
});
}
```
### Where Toasts Are Used
**Error toasts** (shown when operations fail):
- `App.tsx`: Advertisement, channel delete, contact delete
- `useConversationMessages.ts`: Message loading (initial and pagination)
- `MessageInput.tsx`: Message send, telemetry request
- `CrackerPanel.tsx`: Channel save after cracking, WebGPU unavailable
- `StatusBar.tsx`: Manual reconnection failure
- `useWebSocket.ts`: Backend errors via WebSocket `error` events
**Success toasts** (used sparingly for significant actions):
- Radio connection/disconnection status changes
- Backend errors received via WebSocket `error` events
- Manual reconnection success/failure
- Manual reconnection success
- Advertisement sent, channel/contact deleted (confirmation of intentional actions)
**Avoid success toasts** for routine operations like sending messages - only show errors.
The `<Toaster />` component is rendered in `App.tsx` with `position="top-right"`.

File diff suppressed because one or more lines are too long

View File

@@ -13,7 +13,7 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-CBdmGStv.js"></script>
<script type="module" crossorigin src="/assets/index-CTjJAYeA.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DaLCXB8p.css">
</head>
<body>

View File

@@ -352,8 +352,12 @@ export function App() {
const handleAdvertise = useCallback(async () => {
try {
await api.sendAdvertisement(true);
toast.success('Advertisement sent');
} catch (err) {
console.error('Failed to send advertisement:', err);
toast.error('Failed to send advertisement', {
description: err instanceof Error ? err.message : 'Check radio connection',
});
}
}, []);
@@ -375,8 +379,12 @@ export function App() {
await api.deleteChannel(key);
setChannels((prev) => prev.filter((c) => c.key !== key));
setActiveConversation(null);
toast.success('Channel deleted');
} catch (err) {
console.error('Failed to delete channel:', err);
toast.error('Failed to delete channel', {
description: err instanceof Error ? err.message : undefined,
});
}
}, []);
@@ -387,8 +395,12 @@ export function App() {
await api.deleteContact(publicKey);
setContacts((prev) => prev.filter((c) => c.public_key !== publicKey));
setActiveConversation(null);
toast.success('Contact deleted');
} catch (err) {
console.error('Failed to delete contact:', err);
toast.error('Failed to delete contact', {
description: err instanceof Error ? err.message : undefined,
});
}
}, []);

View File

@@ -4,6 +4,7 @@ import { ENGLISH_WORDLIST } from 'meshcore-hashtag-cracker/wordlist';
import NoSleep from 'nosleep.js';
import type { RawPacket, Channel } from '../types';
import { api } from '../api';
import { toast } from './ui/sonner';
import { cn } from '@/lib/utils';
/**
@@ -338,6 +339,9 @@ export function CrackerPanel({ packets, channels, onChannelCreate, onRunningChan
}
} catch (err) {
console.error('Failed to create channel or decrypt historical:', err);
toast.error('Failed to save cracked channel', {
description: err instanceof Error ? err.message : 'Channel discovered but could not be saved',
});
}
}
} else {
@@ -386,7 +390,9 @@ export function CrackerPanel({ packets, channels, onChannelCreate, onRunningChan
// Start/stop handlers
const handleStart = () => {
if (!gpuAvailable) {
alert('WebGPU is not available in your browser. Please use Chrome 113+ or Edge 113+.');
toast.error('WebGPU not available', {
description: 'Cracking requires Chrome 113+ or Edge 113+ with WebGPU support.',
});
return;
}
setIsRunning(true);

View File

@@ -1,6 +1,7 @@
import { useState, useCallback, useImperativeHandle, forwardRef, useRef, useMemo, type FormEvent, type KeyboardEvent } from 'react';
import { Input } from './ui/input';
import { Button } from './ui/button';
import { toast } from './ui/sonner';
import { cn } from '@/lib/utils';
// MeshCore message size limits (empirically determined from LoRa packet constraints)
@@ -103,6 +104,9 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(
setText('');
} catch (err) {
console.error('Failed to request telemetry:', err);
toast.error('Failed to request telemetry', {
description: err instanceof Error ? err.message : 'Check radio connection',
});
return;
} finally {
setSending(false);
@@ -117,6 +121,9 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(
setText('');
} catch (err) {
console.error('Failed to send message:', err);
toast.error('Failed to send message', {
description: err instanceof Error ? err.message : 'Check radio connection',
});
return;
} finally {
setSending(false);

View File

@@ -1,4 +1,5 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { toast } from '../components/ui/sonner';
import { api } from '../api';
import type { Conversation, Message } from '../types';
@@ -61,6 +62,9 @@ export function useConversationMessages(
setHasOlderMessages(data.length >= MESSAGE_PAGE_SIZE);
} catch (err) {
console.error('Failed to fetch messages:', err);
toast.error('Failed to load messages', {
description: err instanceof Error ? err.message : 'Check your connection',
});
} finally {
if (showLoading) {
setMessagesLoading(false);
@@ -93,6 +97,9 @@ export function useConversationMessages(
setHasOlderMessages(data.length >= MESSAGE_PAGE_SIZE);
} catch (err) {
console.error('Failed to fetch older messages:', err);
toast.error('Failed to load older messages', {
description: err instanceof Error ? err.message : 'Check your connection',
});
} finally {
setLoadingOlder(false);
}