From 509f2be3a8f37e41104a8bce674983cb43799d18 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 4 May 2025 10:58:38 -0500 Subject: [PATCH] Fixes a lot of "You provided a value prop to a form field without an onChange handler" errors. Reworks websocket connection to be more robust and notify user of connection errors. Will retry if websocket connection dies. --- dispatcharr/consumers.py | 31 +- frontend/src/App.jsx | 16 +- frontend/src/WebSocket.jsx | 498 +++++++++++------- frontend/src/components/Sidebar.jsx | 1 + frontend/src/components/forms/Channel.jsx | 30 +- frontend/src/components/forms/EPG.jsx | 2 +- frontend/src/components/forms/LoginForm.jsx | 14 +- .../src/components/forms/SuperuserForm.jsx | 8 +- frontend/src/pages/Settings.jsx | 22 +- frontend/src/store/auth.jsx | 66 ++- frontend/src/store/settings.jsx | 15 +- 11 files changed, 433 insertions(+), 270 deletions(-) diff --git a/dispatcharr/consumers.py b/dispatcharr/consumers.py index 8d92c4fa..f7d4a47c 100644 --- a/dispatcharr/consumers.py +++ b/dispatcharr/consumers.py @@ -6,12 +6,35 @@ logger = logging.getLogger(__name__) class MyWebSocketConsumer(AsyncWebsocketConsumer): async def connect(self): - await self.accept() - self.room_name = "updates" - await self.channel_layer.group_add(self.room_name, self.channel_name) + try: + await self.accept() + self.room_name = "updates" + await self.channel_layer.group_add(self.room_name, self.channel_name) + # Send a connection confirmation to the client with consistent format + await self.send(text_data=json.dumps({ + 'type': 'connection_established', + 'data': { + 'success': True, + 'message': 'WebSocket connection established successfully' + } + })) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error in WebSocket connect: {str(e)}") + # If an error occurs during connection, attempt to close + try: + await self.close(code=1011) # Internal server error + except: + pass async def disconnect(self, close_code): - await self.channel_layer.group_discard(self.room_name, self.channel_name) + try: + await self.channel_layer.group_discard(self.room_name, self.channel_name) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error in WebSocket disconnect: {str(e)}") async def receive(self, text_data): data = json.loads(text_data) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e13c5af8..1c032ab3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -65,14 +65,20 @@ const App = () => { // Authentication check useEffect(() => { const checkAuth = async () => { - const loggedIn = await initializeAuth(); - if (loggedIn) { - await initData(); - setIsAuthenticated(true); - } else { + try { + const loggedIn = await initializeAuth(); + if (loggedIn) { + await initData(); + setIsAuthenticated(true); + } else { + await logout(); + } + } catch (error) { + console.error("Auth check failed:", error); await logout(); } }; + checkAuth(); }, [initializeAuth, initData, setIsAuthenticated, logout]); diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx index 0533f13a..1e2b0278 100644 --- a/frontend/src/WebSocket.jsx +++ b/frontend/src/WebSocket.jsx @@ -5,13 +5,14 @@ import React, { createContext, useContext, useMemo, + useCallback, } from 'react'; import useStreamsStore from './store/streams'; import { notifications } from '@mantine/notifications'; import useChannelsStore from './store/channels'; import usePlaylistsStore from './store/playlists'; import useEPGsStore from './store/epgs'; -import { Box, Button, Stack } from '@mantine/core'; +import { Box, Button, Stack, Alert } from '@mantine/core'; import API from './api'; export const WebsocketContext = createContext([false, () => { }, null]); @@ -19,6 +20,299 @@ export const WebsocketContext = createContext([false, () => { }, null]); export const WebsocketProvider = ({ children }) => { const [isReady, setIsReady] = useState(false); const [val, setVal] = useState(null); + const ws = useRef(null); + const reconnectTimerRef = useRef(null); + const [reconnectAttempts, setReconnectAttempts] = useState(0); + const [connectionError, setConnectionError] = useState(null); + const maxReconnectAttempts = 5; + const initialBackoffDelay = 1000; // 1 second initial delay + + // Calculate reconnection delay with exponential backoff + const getReconnectDelay = useCallback(() => { + return Math.min(initialBackoffDelay * Math.pow(1.5, reconnectAttempts), 30000); // max 30 seconds + }, [reconnectAttempts]); + + // Clear any existing reconnect timers + const clearReconnectTimer = useCallback(() => { + if (reconnectTimerRef.current) { + clearTimeout(reconnectTimerRef.current); + reconnectTimerRef.current = null; + } + }, []); + + // Function to get WebSocket URL that works with both HTTP and HTTPS + const getWebSocketUrl = useCallback(() => { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = window.location.hostname; + + // WebSockets always run on port 8001 + return `${protocol}//${host}:8001/ws/`; + }, []); + + // Function to handle websocket connection + const connectWebSocket = useCallback(() => { + // Clear any existing timers to avoid multiple reconnection attempts + clearReconnectTimer(); + + // Clear old websocket if exists + if (ws.current) { + // Remove event handlers to prevent duplicate events + ws.current.onclose = null; + ws.current.onerror = null; + ws.current.onopen = null; + ws.current.onmessage = null; + + try { + ws.current.close(); + } catch (e) { + console.warn("Error closing existing WebSocket:", e); + } + } + + try { + console.log(`Attempting WebSocket connection (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})...`); + + // Use the function to get the correct WebSocket URL + const wsUrl = getWebSocketUrl(); + console.log(`Connecting to WebSocket at: ${wsUrl}`); + + // Create new WebSocket connection + const socket = new WebSocket(wsUrl); + + socket.onopen = () => { + console.log("WebSocket connected successfully"); + setIsReady(true); + setConnectionError(null); + setReconnectAttempts(0); + }; + + socket.onerror = (error) => { + console.error("WebSocket connection error:", error); + + // Don't show error notification on initial page load, + // only show it after a connection was established then lost + if (reconnectAttempts > 0 || isReady) { + setConnectionError("Failed to connect to WebSocket server."); + } else { + console.log("Initial connection attempt failed, will retry..."); + } + }; + + socket.onclose = (event) => { + console.warn("WebSocket connection closed", event); + setIsReady(false); + + // Only attempt reconnect if we haven't reached max attempts + if (reconnectAttempts < maxReconnectAttempts) { + const delay = getReconnectDelay(); + setConnectionError(`Connection lost. Reconnecting in ${Math.ceil(delay / 1000)} seconds...`); + console.log(`Scheduling reconnect in ${delay}ms (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})...`); + + // Store timer reference so we can cancel it if needed + reconnectTimerRef.current = setTimeout(() => { + setReconnectAttempts(prev => prev + 1); + connectWebSocket(); + }, delay); + } else { + setConnectionError("Maximum reconnection attempts reached. Please reload the page."); + console.error("Maximum reconnection attempts reached. WebSocket connection failed."); + } + }; + + // Message handler + socket.onmessage = async (event) => { + try { + const parsedEvent = JSON.parse(event.data); + + // Handle connection_established event + if (parsedEvent.type === 'connection_established') { + console.log('WebSocket connection established:', parsedEvent.data?.message); + // Don't need to do anything else for this event type + return; + } + + // Handle standard message format for other event types + switch (parsedEvent.data?.type) { + case 'epg_file': + fetchEPGs(); + notifications.show({ + title: 'EPG File Detected', + message: `Processing ${parsedEvent.data.filename}`, + }); + break; + + case 'm3u_file': + fetchPlaylists(); + notifications.show({ + title: 'M3U File Detected', + message: `Processing ${parsedEvent.data.filename}`, + }); + break; + + case 'm3u_group_refresh': + fetchChannelGroups(); + fetchPlaylists(); + + notifications.show({ + title: 'Group processing finished!', + autoClose: 5000, + message: ( + + Refresh M3U or filter out groups to pull in streams. + + + ), + color: 'green.5', + }); + break; + + case 'm3u_refresh': + setRefreshProgress(parsedEvent.data); + break; + + case 'channel_stats': + setChannelStats(JSON.parse(parsedEvent.data.stats)); + break; + + case 'epg_channels': + notifications.show({ + message: 'EPG channels updated!', + color: 'green.5', + }); + + // If source_id is provided, update that specific EPG's status + if (parsedEvent.data.source_id) { + const epgsState = useEPGsStore.getState(); + const epg = epgsState.epgs[parsedEvent.data.source_id]; + if (epg) { + epgsState.updateEPG({ + ...epg, + status: 'success' + }); + } + } + + fetchEPGData(); + break; + + case 'epg_match': + notifications.show({ + message: parsedEvent.data.message || 'EPG match is complete!', + color: 'green.5', + }); + + // Check if we have associations data and use the more efficient batch API + if (parsedEvent.data.associations && parsedEvent.data.associations.length > 0) { + API.batchSetEPG(parsedEvent.data.associations); + } + break; + + case 'm3u_profile_test': + setProfilePreview(parsedEvent.data.search_preview, parsedEvent.data.result); + break; + + case 'recording_started': + notifications.show({ + title: 'Recording started!', + message: `Started recording channel ${parsedEvent.data.channel}`, + }); + break; + + case 'recording_ended': + notifications.show({ + title: 'Recording finished!', + message: `Stopped recording channel ${parsedEvent.data.channel}`, + }); + break; + + case 'epg_fetch_error': + notifications.show({ + title: 'EPG Source Error', + message: parsedEvent.data.message, + color: 'orange.5', + autoClose: 8000, + }); + + // Update EPG status in store + if (parsedEvent.data.source_id) { + const epgsState = useEPGsStore.getState(); + const epg = epgsState.epgs[parsedEvent.data.source_id]; + if (epg) { + epgsState.updateEPG({ + ...epg, + status: 'error', + last_error: parsedEvent.data.message + }); + } + } + break; + + case 'epg_refresh': + // Update the store with progress information + const epgsState = useEPGsStore.getState(); + epgsState.updateEPGProgress(parsedEvent.data); + + // If progress is complete (100%), show a notification and refresh EPG data + if (parsedEvent.data.progress === 100 && parsedEvent.data.action === "parsing_programs") { + notifications.show({ + title: 'EPG Processing Complete', + message: 'EPG data has been updated successfully', + color: 'green.5', + }); + + fetchEPGData(); + } + break; + + default: + console.error(`Unknown websocket event type: ${parsedEvent.data?.type}`); + break; + } + } catch (error) { + console.error('Error processing WebSocket message:', error, event.data); + } + }; + + ws.current = socket; + } catch (error) { + console.error("Error creating WebSocket connection:", error); + setConnectionError(`WebSocket error: ${error.message}`); + + // Schedule a reconnect if we haven't reached max attempts + if (reconnectAttempts < maxReconnectAttempts) { + const delay = getReconnectDelay(); + reconnectTimerRef.current = setTimeout(() => { + setReconnectAttempts(prev => prev + 1); + connectWebSocket(); + }, delay); + } + } + }, [reconnectAttempts, clearReconnectTimer, getReconnectDelay, getWebSocketUrl, isReady]); + + // Initial connection and cleanup + useEffect(() => { + connectWebSocket(); + + return () => { + clearReconnectTimer(); // Clear any pending reconnect timers + + if (ws.current) { + console.log("Closing WebSocket connection due to component unmount"); + ws.current.onclose = null; // Remove handlers to avoid reconnection + ws.current.close(); + ws.current = null; + } + }; + }, [connectWebSocket, clearReconnectTimer]); const setChannelStats = useChannelsStore((s) => s.setChannelStats); const fetchChannelGroups = useChannelsStore((s) => s.fetchChannelGroups); @@ -28,198 +322,28 @@ export const WebsocketProvider = ({ children }) => { const fetchEPGData = useEPGsStore((s) => s.fetchEPGData); const fetchEPGs = useEPGsStore((s) => s.fetchEPGs); - const ws = useRef(null); - - useEffect(() => { - let wsUrl = `${window.location.host}/ws/`; - if (import.meta.env.DEV) { - wsUrl = `${window.location.hostname}:8001/ws/`; - } - - if (window.location.protocol.match(/https/)) { - wsUrl = `wss://${wsUrl}`; - } else { - wsUrl = `ws://${wsUrl}`; - } - - const socket = new WebSocket(wsUrl); - - socket.onopen = () => { - console.log('websocket connected'); - setIsReady(true); - }; - - // Reconnection logic - socket.onclose = () => { - setIsReady(false); - setTimeout(() => { - const reconnectWs = new WebSocket(wsUrl); - reconnectWs.onopen = () => setIsReady(true); - }, 3000); // Attempt to reconnect every 3 seconds - }; - - socket.onmessage = async (event) => { - event = JSON.parse(event.data); - switch (event.data.type) { - case 'epg_file': - fetchEPGs(); - notifications.show({ - title: 'EPG File Detected', - message: `Processing ${event.data.filename}`, - }); - break; - - case 'm3u_file': - fetchPlaylists(); - notifications.show({ - title: 'M3U File Detected', - message: `Processing ${event.data.filename}`, - }); - break; - - case 'm3u_group_refresh': - fetchChannelGroups(); - fetchPlaylists(); - - notifications.show({ - title: 'Group processing finished!', - autoClose: 5000, - message: ( - - Refresh M3U or filter out groups to pull in streams. - - - ), - color: 'green.5', - }); - break; - - case 'm3u_refresh': - setRefreshProgress(event.data); - break; - - case 'channel_stats': - setChannelStats(JSON.parse(event.data.stats)); - break; - - case 'epg_channels': - notifications.show({ - message: 'EPG channels updated!', - color: 'green.5', - }); - - // If source_id is provided, update that specific EPG's status - if (event.data.source_id) { - const epgsState = useEPGsStore.getState(); - const epg = epgsState.epgs[event.data.source_id]; - if (epg) { - epgsState.updateEPG({ - ...epg, - status: 'success' - }); - } - } - - fetchEPGData(); - break; - - case 'epg_match': - notifications.show({ - message: event.data.message || 'EPG match is complete!', - color: 'green.5', - }); - - // Check if we have associations data and use the more efficient batch API - if (event.data.associations && event.data.associations.length > 0) { - API.batchSetEPG(event.data.associations); - } - break; - - case 'm3u_profile_test': - setProfilePreview(event.data.search_preview, event.data.result); - break; - - case 'recording_started': - notifications.show({ - title: 'Recording started!', - message: `Started recording channel ${event.data.channel}`, - }); - break; - - case 'recording_ended': - notifications.show({ - title: 'Recording finished!', - message: `Stopped recording channel ${event.data.channel}`, - }); - break; - - case 'epg_fetch_error': - notifications.show({ - title: 'EPG Source Error', - message: event.data.message, - color: 'orange.5', - autoClose: 8000, - }); - - // Update EPG status in store - if (event.data.source_id) { - const epgsState = useEPGsStore.getState(); - const epg = epgsState.epgs[event.data.source_id]; - if (epg) { - epgsState.updateEPG({ - ...epg, - status: 'error', - last_error: event.data.message - }); - } - } - break; - - case 'epg_refresh': - // Update the store with progress information - const epgsState = useEPGsStore.getState(); - epgsState.updateEPGProgress(event.data); - - // If progress is complete (100%), show a notification and refresh EPG data - if (event.data.progress === 100 && event.data.action === "parsing_programs") { - notifications.show({ - title: 'EPG Processing Complete', - message: 'EPG data has been updated successfully', - color: 'green.5', - }); - - fetchEPGData(); - } - break; - - default: - console.error(`Unknown websocket event type: ${event.type}`); - break; - } - }; - - ws.current = socket; - - return () => { - socket.close(); - }; - }, []); - const ret = useMemo(() => { return [isReady, ws.current?.send.bind(ws.current), val]; }, [isReady, val]); return ( + {connectionError && !isReady && reconnectAttempts >= maxReconnectAttempts && ( + + {connectionError} + + + )} + {connectionError && !isReady && reconnectAttempts < maxReconnectAttempts && reconnectAttempts > 0 && ( + + {connectionError} + + )} {children} ); diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index eb5a2226..688ce3a6 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -218,6 +218,7 @@ const Sidebar = ({ collapsed, toggleDrawer, drawerWidth, miniDrawerWidth }) => { label="Public IP" ref={publicIPRef} value={environment.public_ip} + readOnly={true} leftSection={ environment.country_code && ( { +const ChannelForm = ({ channel = null, isOpen, onClose }) => { const theme = useMantineTheme(); const listRef = useRef(null); @@ -59,7 +59,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => { const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false); const [epgPopoverOpened, setEpgPopoverOpened] = useState(false); const [logoPopoverOpened, setLogoPopoverOpened] = useState(false); - const [selectedEPG, setSelectedEPG] = useState({}); + const [selectedEPG, setSelectedEPG] = useState(''); const [tvgFilter, setTvgFilter] = useState(''); const [logoFilter, setLogoFilter] = useState(''); const [logoOptions, setLogoOptions] = useState([]); @@ -94,11 +94,11 @@ const Channel = ({ channel = null, isOpen, onClose }) => { const formik = useFormik({ initialValues: { name: '', - channel_number: 0, - channel_group_id: Object.keys(channelGroups)[0], + channel_number: '', // Change from 0 to empty string for consistency + channel_group_id: Object.keys(channelGroups).length > 0 ? Object.keys(channelGroups)[0] : '', stream_profile_id: '0', tvg_id: '', - tvc_guide_stationid: '', + tvc_guide_stationid: '', epg_data_id: '', logo_id: '', }, @@ -177,26 +177,26 @@ const Channel = ({ channel = null, isOpen, onClose }) => { useEffect(() => { if (channel) { if (channel.epg_data_id) { - const epgSource = epgs[tvgsById[channel.epg_data_id].epg_source]; - setSelectedEPG(`${epgSource.id}`); + const epgSource = epgs[tvgsById[channel.epg_data_id]?.epg_source]; + setSelectedEPG(epgSource ? `${epgSource.id}` : ''); } formik.setValues({ - name: channel.name, - channel_number: channel.channel_number, + name: channel.name || '', + channel_number: channel.channel_number !== null ? channel.channel_number : '', channel_group_id: channel.channel_group_id ? `${channel.channel_group_id}` : '', stream_profile_id: channel.stream_profile_id ? `${channel.stream_profile_id}` : '0', - tvg_id: channel.tvg_id, - tvc_guide_stationid: channel.tvc_guide_stationid, + tvg_id: channel.tvg_id || '', + tvc_guide_stationid: channel.tvc_guide_stationid || '', epg_data_id: channel.epg_data_id ?? '', - logo_id: `${channel.logo_id}`, + logo_id: channel.logo_id ? `${channel.logo_id}` : '', }); - setChannelStreams(channel.streams); + setChannelStreams(channel.streams || []); } else { formik.resetForm(); setTvgFilter(''); @@ -678,7 +678,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => { error={formik.errors.tvg_id ? formik.touched.tvg_id : ''} size="xs" /> - + { ); }; -export default Channel; +export default ChannelForm; diff --git a/frontend/src/components/forms/EPG.jsx b/frontend/src/components/forms/EPG.jsx index c5d5b6e2..bc4439e3 100644 --- a/frontend/src/components/forms/EPG.jsx +++ b/frontend/src/components/forms/EPG.jsx @@ -134,7 +134,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => { diff --git a/frontend/src/components/forms/SuperuserForm.jsx b/frontend/src/components/forms/SuperuserForm.jsx index 1af4e8d4..fbcf0eaa 100644 --- a/frontend/src/components/forms/SuperuserForm.jsx +++ b/frontend/src/components/forms/SuperuserForm.jsx @@ -4,7 +4,7 @@ import { TextInput, Center, Button, Paper, Title, Stack } from '@mantine/core'; import API from '../../api'; import useAuthStore from '../../store/auth'; -function SuperuserForm({}) { +function SuperuserForm() { const [formData, setFormData] = useState({ username: '', password: '', @@ -34,11 +34,7 @@ function SuperuserForm({}) { } } catch (err) { console.log(err); - // let msg = 'Failed to create superuser.'; - // if (err.response && err.response.data && err.response.data.error) { - // msg += ` ${err.response.data.error}`; - // } - // setError(msg); + setError('Failed to create superuser.'); } }; diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 3903bc31..618261bc 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -290,9 +290,9 @@ const SettingsPage = () => { }, validate: { - 'default-user-agent': isNotEmpty('Select a channel'), - 'default-stream-profile': isNotEmpty('Select a start time'), - 'preferred-region': isNotEmpty('Select an end time'), + 'default-user-agent': isNotEmpty('Select a user agent'), + 'default-stream-profile': isNotEmpty('Select a stream profile'), + 'preferred-region': isNotEmpty('Select a region'), }, }); @@ -399,9 +399,9 @@ const SettingsPage = () => { searchable {...form.getInputProps('default-user-agent')} key={form.key('default-user-agent')} - id={settings['default-user-agent']?.id} - name={settings['default-user-agent']?.key} - label={settings['default-user-agent']?.name} + id={settings['default-user-agent']?.id || 'default-user-agent'} + name={settings['default-user-agent']?.key || 'default-user-agent'} + label={settings['default-user-agent']?.name || 'Default User Agent'} data={userAgents.map((option) => ({ value: `${option.id}`, label: option.name, @@ -412,9 +412,9 @@ const SettingsPage = () => { searchable {...form.getInputProps('default-stream-profile')} key={form.key('default-stream-profile')} - id={settings['default-stream-profile']?.id} - name={settings['default-stream-profile']?.key} - label={settings['default-stream-profile']?.name} + id={settings['default-stream-profile']?.id || 'default-stream-profile'} + name={settings['default-stream-profile']?.key || 'default-stream-profile'} + label={settings['default-stream-profile']?.name || 'Default Stream Profile'} data={streamProfiles.map((option) => ({ value: `${option.id}`, label: option.name, @@ -426,9 +426,7 @@ const SettingsPage = () => { key={form.key('preferred-region')} id={settings['preferred-region']?.id || 'preferred-region'} name={settings['preferred-region']?.key || 'preferred-region'} - label={ - settings['preferred-region']?.name || 'Preferred Region' - } + label={settings['preferred-region']?.name || 'Preferred Region'} data={regionChoices.map((r) => ({ label: r.label, value: `${r.value}`, diff --git a/frontend/src/store/auth.jsx b/frontend/src/store/auth.jsx index d6eb8053..a55b77d4 100644 --- a/frontend/src/store/auth.jsx +++ b/frontend/src/store/auth.jsx @@ -1,11 +1,11 @@ import { create } from 'zustand'; -import API from '../api'; +import api from '../api'; +import useSettingsStore from './settings'; import useChannelsStore from './channels'; -import useUserAgentsStore from './userAgents'; import usePlaylistsStore from './playlists'; import useEPGsStore from './epgs'; import useStreamProfilesStore from './streamProfiles'; -import useSettingsStore from './settings'; +import useUserAgentsStore from './userAgents'; const decodeToken = (token) => { if (!token) return null; @@ -20,32 +20,46 @@ const isTokenExpired = (expirationTime) => { }; const useAuthStore = create((set, get) => ({ + isAuthenticated: false, + isInitialized: false, + needsSuperuser: false, + user: { + username: '', + email: '', + }, + isLoading: false, + error: null, + + initData: async () => { + // Ensure settings are loaded first + await useSettingsStore.getState().fetchSettings(); + + try { + // Only after settings are loaded, fetch the dependent data + await Promise.all([ + useChannelsStore.getState().fetchChannels(), + useChannelsStore.getState().fetchChannelGroups(), + usePlaylistsStore.getState().fetchPlaylists(), + useEPGsStore.getState().fetchEPGs(), + useEPGsStore.getState().fetchEPGData(), + useChannelsStore.getState().fetchLogos(), + useStreamProfilesStore.getState().fetchProfiles(), + useUserAgentsStore.getState().fetchUserAgents(), + ]); + } catch (error) { + console.error("Error initializing data:", error); + } + }, + accessToken: localStorage.getItem('accessToken') || null, refreshToken: localStorage.getItem('refreshToken') || null, tokenExpiration: localStorage.getItem('tokenExpiration') || null, superuserExists: true, - isAuthenticated: false, setIsAuthenticated: (isAuthenticated) => set({ isAuthenticated }), setSuperuserExists: (superuserExists) => set({ superuserExists }), - initData: async () => { - await Promise.all([ - useChannelsStore.getState().fetchChannels(), - useChannelsStore.getState().fetchChannelGroups(), - useChannelsStore.getState().fetchLogos(), - useChannelsStore.getState().fetchChannelProfiles(), - useChannelsStore.getState().fetchRecordings(), - useUserAgentsStore.getState().fetchUserAgents(), - usePlaylistsStore.getState().fetchPlaylists(), - useEPGsStore.getState().fetchEPGs(), - useEPGsStore.getState().fetchEPGData(), - useStreamProfilesStore.getState().fetchProfiles(), - useSettingsStore.getState().fetchSettings(), - ]); - }, - getToken: async () => { const tokenExpiration = localStorage.getItem('tokenExpiration'); let accessToken = null; @@ -61,7 +75,7 @@ const useAuthStore = create((set, get) => ({ // Action to login login: async ({ username, password }) => { try { - const response = await API.login(username, password); + const response = await api.login(username, password); if (response.access) { const expiration = decodeToken(response.access); set({ @@ -83,11 +97,11 @@ const useAuthStore = create((set, get) => ({ // Action to refresh the token getRefreshToken: async () => { const refreshToken = localStorage.getItem('refreshToken'); - if (!refreshToken) return; + if (!refreshToken) return false; // Add explicit return here try { - const data = await API.refreshToken(refreshToken); - if (data.access) { + const data = await api.refreshToken(refreshToken); + if (data && data.access) { set({ accessToken: data.access, tokenExpiration: decodeToken(data.access), @@ -98,12 +112,12 @@ const useAuthStore = create((set, get) => ({ return data.access; } + return false; // Add explicit return for when data.access is not available } catch (error) { console.error('Token refresh failed:', error); get().logout(); + return false; // Add explicit return after error } - - return false; }, // Action to logout diff --git a/frontend/src/store/settings.jsx b/frontend/src/store/settings.jsx index 5dffbed6..99390320 100644 --- a/frontend/src/store/settings.jsx +++ b/frontend/src/store/settings.jsx @@ -3,7 +3,13 @@ import api from '../api'; const useSettingsStore = create((set) => ({ settings: {}, - environment: {}, + environment: { + // Add default values for environment settings + public_ip: '', + country_code: '', + country_name: '', + env_mode: 'prod', + }, isLoading: false, error: null, @@ -18,7 +24,12 @@ const useSettingsStore = create((set) => ({ return acc; }, {}), isLoading: false, - environment: env, + environment: env || { + public_ip: '', + country_code: '', + country_name: '', + env_mode: 'prod', + }, }); } catch (error) { set({ error: 'Failed to load settings.', isLoading: false });