mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-08 01:33:01 +02:00
Frontend overhaul
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
export { useRepeaterMode } from './useRepeaterMode';
|
||||
export { useUnreadCounts } from './useUnreadCounts';
|
||||
export { useConversationMessages, getMessageContentKey } from './useConversationMessages';
|
||||
export { useRadioControl } from './useRadioControl';
|
||||
export { useAppSettings } from './useAppSettings';
|
||||
export { useConversationRouter } from './useConversationRouter';
|
||||
export { useContactsAndChannels } from './useContactsAndChannels';
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { api } from '../api';
|
||||
import { takePrefetch } from '../prefetch';
|
||||
import { toast } from '../components/ui/sonner';
|
||||
import {
|
||||
initLastMessageTimes,
|
||||
loadLocalStorageLastMessageTimes,
|
||||
loadLocalStorageSortOrder,
|
||||
clearLocalStorageConversationState,
|
||||
} from '../utils/conversationState';
|
||||
import {
|
||||
isFavorite,
|
||||
loadLocalStorageFavorites,
|
||||
clearLocalStorageFavorites,
|
||||
} from '../utils/favorites';
|
||||
import type { AppSettings, AppSettingsUpdate, Favorite } from '../types';
|
||||
|
||||
export function useAppSettings() {
|
||||
const [appSettings, setAppSettings] = useState<AppSettings | null>(null);
|
||||
|
||||
// Stable empty array prevents a new reference every render when there are none.
|
||||
const emptyFavorites = useRef<Favorite[]>([]).current;
|
||||
const favorites: Favorite[] = appSettings?.favorites ?? emptyFavorites;
|
||||
|
||||
// One-time migration guard
|
||||
const hasMigratedRef = useRef(false);
|
||||
|
||||
const fetchAppSettings = useCallback(async () => {
|
||||
try {
|
||||
const data = await (takePrefetch('settings') ?? api.getSettings());
|
||||
setAppSettings(data);
|
||||
initLastMessageTimes(data.last_message_times ?? {});
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch app settings:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSaveAppSettings = useCallback(
|
||||
async (update: AppSettingsUpdate) => {
|
||||
await api.updateSettings(update);
|
||||
await fetchAppSettings();
|
||||
},
|
||||
[fetchAppSettings]
|
||||
);
|
||||
|
||||
const handleSortOrderChange = useCallback(
|
||||
async (order: 'recent' | 'alpha') => {
|
||||
const previousOrder = appSettings?.sidebar_sort_order ?? 'recent';
|
||||
|
||||
// Optimistic update for responsive UI
|
||||
setAppSettings((prev) => (prev ? { ...prev, sidebar_sort_order: order } : prev));
|
||||
|
||||
try {
|
||||
const updatedSettings = await api.updateSettings({ sidebar_sort_order: order });
|
||||
setAppSettings(updatedSettings);
|
||||
} catch (err) {
|
||||
console.error('Failed to update sort order:', err);
|
||||
setAppSettings((prev) => (prev ? { ...prev, sidebar_sort_order: previousOrder } : prev));
|
||||
toast.error('Failed to save sort preference');
|
||||
}
|
||||
},
|
||||
[appSettings?.sidebar_sort_order]
|
||||
);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (type: 'channel' | 'contact', id: string) => {
|
||||
setAppSettings((prev) => {
|
||||
if (!prev) return prev;
|
||||
const currentFavorites = prev.favorites ?? [];
|
||||
const wasFavorited = isFavorite(currentFavorites, type, id);
|
||||
const optimisticFavorites = wasFavorited
|
||||
? currentFavorites.filter((f) => !(f.type === type && f.id === id))
|
||||
: [...currentFavorites, { type, id }];
|
||||
return { ...prev, favorites: optimisticFavorites };
|
||||
});
|
||||
|
||||
try {
|
||||
const updatedSettings = await api.toggleFavorite(type, id);
|
||||
setAppSettings(updatedSettings);
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle favorite:', err);
|
||||
try {
|
||||
const settings = await api.getSettings();
|
||||
setAppSettings(settings);
|
||||
} catch {
|
||||
// If refetch also fails, leave optimistic state
|
||||
}
|
||||
toast.error('Failed to update favorite');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// One-time migration of localStorage preferences to server
|
||||
useEffect(() => {
|
||||
if (!appSettings || hasMigratedRef.current) return;
|
||||
|
||||
if (appSettings.preferences_migrated) {
|
||||
clearLocalStorageFavorites();
|
||||
clearLocalStorageConversationState();
|
||||
hasMigratedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const localFavorites = loadLocalStorageFavorites();
|
||||
const localSortOrder = loadLocalStorageSortOrder();
|
||||
const localLastMessageTimes = loadLocalStorageLastMessageTimes();
|
||||
|
||||
const hasLocalData =
|
||||
localFavorites.length > 0 ||
|
||||
localSortOrder !== 'recent' ||
|
||||
Object.keys(localLastMessageTimes).length > 0;
|
||||
|
||||
if (!hasLocalData) {
|
||||
hasMigratedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
hasMigratedRef.current = true;
|
||||
|
||||
const migratePreferences = async () => {
|
||||
try {
|
||||
const result = await api.migratePreferences({
|
||||
favorites: localFavorites,
|
||||
sort_order: localSortOrder,
|
||||
last_message_times: localLastMessageTimes,
|
||||
});
|
||||
|
||||
if (result.migrated) {
|
||||
toast.success('Preferences migrated', {
|
||||
description: `Migrated ${localFavorites.length} favorites to server`,
|
||||
});
|
||||
}
|
||||
|
||||
setAppSettings(result.settings);
|
||||
initLastMessageTimes(result.settings.last_message_times ?? {});
|
||||
|
||||
clearLocalStorageFavorites();
|
||||
clearLocalStorageConversationState();
|
||||
} catch (err) {
|
||||
console.error('Failed to migrate preferences:', err);
|
||||
}
|
||||
};
|
||||
|
||||
migratePreferences();
|
||||
}, [appSettings]);
|
||||
|
||||
return {
|
||||
appSettings,
|
||||
setAppSettings,
|
||||
favorites,
|
||||
fetchAppSettings,
|
||||
handleSaveAppSettings,
|
||||
handleSortOrderChange,
|
||||
handleToggleFavorite,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { useState, useCallback, type MutableRefObject } from 'react';
|
||||
import { api } from '../api';
|
||||
import { takePrefetch } from '../prefetch';
|
||||
import { toast } from '../components/ui/sonner';
|
||||
import * as messageCache from '../messageCache';
|
||||
import { getContactDisplayName } from '../utils/pubkey';
|
||||
import type { Channel, Contact, Conversation } from '../types';
|
||||
|
||||
const PUBLIC_CHANNEL_KEY = '8B3387E9C5CDEA6AC9E5EDBAA115CD72';
|
||||
|
||||
interface UseContactsAndChannelsArgs {
|
||||
setActiveConversation: (conv: Conversation | null) => void;
|
||||
pendingDeleteFallbackRef: MutableRefObject<boolean>;
|
||||
hasSetDefaultConversation: MutableRefObject<boolean>;
|
||||
}
|
||||
|
||||
export function useContactsAndChannels({
|
||||
setActiveConversation,
|
||||
pendingDeleteFallbackRef,
|
||||
hasSetDefaultConversation,
|
||||
}: UseContactsAndChannelsArgs) {
|
||||
const [contacts, setContacts] = useState<Contact[]>([]);
|
||||
const [contactsLoaded, setContactsLoaded] = useState(false);
|
||||
const [channels, setChannels] = useState<Channel[]>([]);
|
||||
const [undecryptedCount, setUndecryptedCount] = useState(0);
|
||||
|
||||
const fetchUndecryptedCountInternal = useCallback(async () => {
|
||||
try {
|
||||
const data = await (takePrefetch('undecryptedCount') ?? api.getUndecryptedPacketCount());
|
||||
setUndecryptedCount(data.count);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch undecrypted count:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch all contacts, paginating if >1000
|
||||
const fetchAllContacts = useCallback(async (): Promise<Contact[]> => {
|
||||
const pageSize = 1000;
|
||||
const first = await (takePrefetch('contacts') ?? api.getContacts(pageSize, 0));
|
||||
if (first.length < pageSize) return first;
|
||||
let all = [...first];
|
||||
let offset = pageSize;
|
||||
while (true) {
|
||||
const page = await api.getContacts(pageSize, offset);
|
||||
all = all.concat(page);
|
||||
if (page.length < pageSize) break;
|
||||
offset += pageSize;
|
||||
}
|
||||
return all;
|
||||
}, []);
|
||||
|
||||
const handleCreateContact = useCallback(
|
||||
async (name: string, publicKey: string, tryHistorical: boolean) => {
|
||||
const created = await api.createContact(publicKey, name || undefined, tryHistorical);
|
||||
const data = await fetchAllContacts();
|
||||
setContacts(data);
|
||||
|
||||
setActiveConversation({
|
||||
type: 'contact',
|
||||
id: created.public_key,
|
||||
name: getContactDisplayName(created.name, created.public_key),
|
||||
});
|
||||
},
|
||||
[fetchAllContacts, setActiveConversation]
|
||||
);
|
||||
|
||||
const handleCreateChannel = useCallback(
|
||||
async (name: string, key: string, tryHistorical: boolean) => {
|
||||
const created = await api.createChannel(name, key);
|
||||
const data = await api.getChannels();
|
||||
setChannels(data);
|
||||
|
||||
setActiveConversation({
|
||||
type: 'channel',
|
||||
id: created.key,
|
||||
name,
|
||||
});
|
||||
|
||||
if (tryHistorical) {
|
||||
await api.decryptHistoricalPackets({
|
||||
key_type: 'channel',
|
||||
channel_key: created.key,
|
||||
});
|
||||
fetchUndecryptedCountInternal();
|
||||
}
|
||||
},
|
||||
[fetchUndecryptedCountInternal, setActiveConversation]
|
||||
);
|
||||
|
||||
const handleCreateHashtagChannel = useCallback(
|
||||
async (name: string, tryHistorical: boolean) => {
|
||||
const channelName = name.startsWith('#') ? name : `#${name}`;
|
||||
|
||||
const created = await api.createChannel(channelName);
|
||||
const data = await api.getChannels();
|
||||
setChannels(data);
|
||||
|
||||
setActiveConversation({
|
||||
type: 'channel',
|
||||
id: created.key,
|
||||
name: channelName,
|
||||
});
|
||||
|
||||
if (tryHistorical) {
|
||||
await api.decryptHistoricalPackets({
|
||||
key_type: 'channel',
|
||||
channel_name: channelName,
|
||||
});
|
||||
fetchUndecryptedCountInternal();
|
||||
}
|
||||
},
|
||||
[fetchUndecryptedCountInternal, setActiveConversation]
|
||||
);
|
||||
|
||||
const handleDeleteChannel = useCallback(
|
||||
async (key: string) => {
|
||||
if (!confirm('Delete this channel? Message history will be preserved.')) return;
|
||||
try {
|
||||
pendingDeleteFallbackRef.current = true;
|
||||
await api.deleteChannel(key);
|
||||
messageCache.remove(key);
|
||||
const refreshedChannels = await api.getChannels();
|
||||
setChannels(refreshedChannels);
|
||||
const publicChannel =
|
||||
refreshedChannels.find((c) => c.key === PUBLIC_CHANNEL_KEY) ||
|
||||
refreshedChannels.find((c) => c.name === 'Public');
|
||||
hasSetDefaultConversation.current = true;
|
||||
setActiveConversation({
|
||||
type: 'channel',
|
||||
id: publicChannel?.key || PUBLIC_CHANNEL_KEY,
|
||||
name: publicChannel?.name || 'Public',
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setActiveConversation, pendingDeleteFallbackRef, hasSetDefaultConversation]
|
||||
);
|
||||
|
||||
const handleDeleteContact = useCallback(
|
||||
async (publicKey: string) => {
|
||||
if (!confirm('Delete this contact? Message history will be preserved.')) return;
|
||||
try {
|
||||
pendingDeleteFallbackRef.current = true;
|
||||
await api.deleteContact(publicKey);
|
||||
messageCache.remove(publicKey);
|
||||
setContacts((prev) => prev.filter((c) => c.public_key !== publicKey));
|
||||
const refreshedChannels = await api.getChannels();
|
||||
setChannels(refreshedChannels);
|
||||
const publicChannel =
|
||||
refreshedChannels.find((c) => c.key === PUBLIC_CHANNEL_KEY) ||
|
||||
refreshedChannels.find((c) => c.name === 'Public');
|
||||
hasSetDefaultConversation.current = true;
|
||||
setActiveConversation({
|
||||
type: 'channel',
|
||||
id: publicChannel?.key || PUBLIC_CHANNEL_KEY,
|
||||
name: publicChannel?.name || 'Public',
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setActiveConversation, pendingDeleteFallbackRef, hasSetDefaultConversation]
|
||||
);
|
||||
|
||||
return {
|
||||
contacts,
|
||||
contactsLoaded,
|
||||
channels,
|
||||
undecryptedCount,
|
||||
setContacts,
|
||||
setContactsLoaded,
|
||||
setChannels,
|
||||
fetchAllContacts,
|
||||
fetchUndecryptedCount: fetchUndecryptedCountInternal,
|
||||
handleCreateContact,
|
||||
handleCreateChannel,
|
||||
handleCreateHashtagChannel,
|
||||
handleDeleteChannel,
|
||||
handleDeleteContact,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { useState, useCallback, useEffect, useRef, type MutableRefObject } from 'react';
|
||||
import {
|
||||
parseHashConversation,
|
||||
updateUrlHash,
|
||||
resolveChannelFromHashToken,
|
||||
resolveContactFromHashToken,
|
||||
} from '../utils/urlHash';
|
||||
import { getContactDisplayName } from '../utils/pubkey';
|
||||
import type { Channel, Contact, Conversation } from '../types';
|
||||
|
||||
const PUBLIC_CHANNEL_KEY = '8B3387E9C5CDEA6AC9E5EDBAA115CD72';
|
||||
|
||||
interface UseConversationRouterArgs {
|
||||
channels: Channel[];
|
||||
contacts: Contact[];
|
||||
contactsLoaded: boolean;
|
||||
setSidebarOpen: (open: boolean) => void;
|
||||
pendingDeleteFallbackRef: MutableRefObject<boolean>;
|
||||
hasSetDefaultConversation: MutableRefObject<boolean>;
|
||||
}
|
||||
|
||||
export function useConversationRouter({
|
||||
channels,
|
||||
contacts,
|
||||
contactsLoaded,
|
||||
setSidebarOpen,
|
||||
pendingDeleteFallbackRef,
|
||||
hasSetDefaultConversation,
|
||||
}: UseConversationRouterArgs) {
|
||||
const [activeConversation, setActiveConversation] = useState<Conversation | null>(null);
|
||||
const activeConversationRef = useRef<Conversation | null>(null);
|
||||
|
||||
// Phase 1: Set initial conversation from URL hash or default to Public channel
|
||||
// Only needs channels (fast path) - doesn't wait for contacts
|
||||
useEffect(() => {
|
||||
if (hasSetDefaultConversation.current || activeConversation) return;
|
||||
if (channels.length === 0) return;
|
||||
|
||||
const hashConv = parseHashConversation();
|
||||
|
||||
// Handle non-data views immediately
|
||||
if (hashConv?.type === 'raw') {
|
||||
setActiveConversation({ type: 'raw', id: 'raw', name: 'Raw Packet Feed' });
|
||||
hasSetDefaultConversation.current = true;
|
||||
return;
|
||||
}
|
||||
if (hashConv?.type === 'map') {
|
||||
setActiveConversation({
|
||||
type: 'map',
|
||||
id: 'map',
|
||||
name: 'Node Map',
|
||||
mapFocusKey: hashConv.mapFocusKey,
|
||||
});
|
||||
hasSetDefaultConversation.current = true;
|
||||
return;
|
||||
}
|
||||
if (hashConv?.type === 'visualizer') {
|
||||
setActiveConversation({ type: 'visualizer', id: 'visualizer', name: 'Mesh Visualizer' });
|
||||
hasSetDefaultConversation.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle channel hash (ID-first with legacy-name fallback)
|
||||
if (hashConv?.type === 'channel') {
|
||||
const channel = resolveChannelFromHashToken(hashConv.name, channels);
|
||||
if (channel) {
|
||||
setActiveConversation({ type: 'channel', id: channel.key, name: channel.name });
|
||||
hasSetDefaultConversation.current = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Contact hash — wait for phase 2
|
||||
if (hashConv?.type === 'contact') return;
|
||||
|
||||
// No hash or unresolvable — default to Public
|
||||
const publicChannel = channels.find((c) => c.name === 'Public');
|
||||
if (publicChannel) {
|
||||
setActiveConversation({
|
||||
type: 'channel',
|
||||
id: publicChannel.key,
|
||||
name: publicChannel.name,
|
||||
});
|
||||
hasSetDefaultConversation.current = true;
|
||||
}
|
||||
}, [channels, activeConversation]);
|
||||
|
||||
// Phase 2: Resolve contact hash (only if phase 1 didn't set a conversation)
|
||||
useEffect(() => {
|
||||
if (hasSetDefaultConversation.current || activeConversation) return;
|
||||
|
||||
const hashConv = parseHashConversation();
|
||||
if (hashConv?.type === 'contact') {
|
||||
if (!contactsLoaded) return;
|
||||
|
||||
const contact = resolveContactFromHashToken(hashConv.name, contacts);
|
||||
if (contact) {
|
||||
setActiveConversation({
|
||||
type: 'contact',
|
||||
id: contact.public_key,
|
||||
name: getContactDisplayName(contact.name, contact.public_key),
|
||||
});
|
||||
hasSetDefaultConversation.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Contact hash didn't match — fall back to Public if channels loaded.
|
||||
if (channels.length > 0) {
|
||||
const publicChannel = channels.find((c) => c.name === 'Public');
|
||||
if (publicChannel) {
|
||||
setActiveConversation({
|
||||
type: 'channel',
|
||||
id: publicChannel.key,
|
||||
name: publicChannel.name,
|
||||
});
|
||||
hasSetDefaultConversation.current = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [contacts, channels, activeConversation, contactsLoaded]);
|
||||
|
||||
// Keep ref in sync and update URL hash
|
||||
useEffect(() => {
|
||||
activeConversationRef.current = activeConversation;
|
||||
if (activeConversation) {
|
||||
updateUrlHash(activeConversation);
|
||||
}
|
||||
}, [activeConversation]);
|
||||
|
||||
// If a delete action left us without an active conversation, recover to Public
|
||||
useEffect(() => {
|
||||
if (!pendingDeleteFallbackRef.current) return;
|
||||
if (activeConversation) {
|
||||
pendingDeleteFallbackRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const publicChannel =
|
||||
channels.find((c) => c.key === PUBLIC_CHANNEL_KEY) ||
|
||||
channels.find((c) => c.name === 'Public');
|
||||
if (!publicChannel) return;
|
||||
|
||||
hasSetDefaultConversation.current = true;
|
||||
pendingDeleteFallbackRef.current = false;
|
||||
setActiveConversation({
|
||||
type: 'channel',
|
||||
id: publicChannel.key,
|
||||
name: publicChannel.name,
|
||||
});
|
||||
}, [activeConversation, channels]);
|
||||
|
||||
// Handle conversation selection (closes sidebar on mobile)
|
||||
const handleSelectConversation = useCallback(
|
||||
(conv: Conversation) => {
|
||||
setActiveConversation(conv);
|
||||
setSidebarOpen(false);
|
||||
},
|
||||
[setSidebarOpen]
|
||||
);
|
||||
|
||||
return {
|
||||
activeConversation,
|
||||
setActiveConversation,
|
||||
activeConversationRef,
|
||||
handleSelectConversation,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { api } from '../api';
|
||||
import { takePrefetch } from '../prefetch';
|
||||
import { toast } from '../components/ui/sonner';
|
||||
import type { HealthStatus, RadioConfig, RadioConfigUpdate } from '../types';
|
||||
|
||||
export function useRadioControl() {
|
||||
const [health, setHealth] = useState<HealthStatus | null>(null);
|
||||
const [config, setConfig] = useState<RadioConfig | null>(null);
|
||||
|
||||
const prevHealthRef = useRef<HealthStatus | null>(null);
|
||||
const rebootPollTokenRef = useRef(0);
|
||||
|
||||
// Cancel any in-flight reboot polling on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
rebootPollTokenRef.current += 1;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchConfig = useCallback(async () => {
|
||||
try {
|
||||
const data = await (takePrefetch('config') ?? api.getRadioConfig());
|
||||
setConfig(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch config:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSaveConfig = useCallback(
|
||||
async (update: RadioConfigUpdate) => {
|
||||
await api.updateRadioConfig(update);
|
||||
await fetchConfig();
|
||||
},
|
||||
[fetchConfig]
|
||||
);
|
||||
|
||||
const handleSetPrivateKey = useCallback(
|
||||
async (key: string) => {
|
||||
await api.setPrivateKey(key);
|
||||
await fetchConfig();
|
||||
},
|
||||
[fetchConfig]
|
||||
);
|
||||
|
||||
const handleReboot = useCallback(async () => {
|
||||
await api.rebootRadio();
|
||||
setHealth((prev) => (prev ? { ...prev, radio_connected: false } : prev));
|
||||
const pollToken = ++rebootPollTokenRef.current;
|
||||
const pollUntilReconnected = async () => {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
if (rebootPollTokenRef.current !== pollToken) return;
|
||||
try {
|
||||
const data = await api.getHealth();
|
||||
if (rebootPollTokenRef.current !== pollToken) return;
|
||||
setHealth(data);
|
||||
if (data.radio_connected) {
|
||||
fetchConfig();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Keep polling
|
||||
}
|
||||
}
|
||||
};
|
||||
pollUntilReconnected();
|
||||
}, [fetchConfig]);
|
||||
|
||||
const handleAdvertise = useCallback(async () => {
|
||||
try {
|
||||
await api.sendAdvertisement();
|
||||
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',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleHealthRefresh = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.getHealth();
|
||||
setHealth(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh health:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
health,
|
||||
setHealth,
|
||||
config,
|
||||
setConfig,
|
||||
prevHealthRef,
|
||||
fetchConfig,
|
||||
handleSaveConfig,
|
||||
handleSetPrivateKey,
|
||||
handleReboot,
|
||||
handleAdvertise,
|
||||
handleHealthRefresh,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user