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.
This commit is contained in:
SergeantPanda 2025-05-04 10:58:38 -05:00
parent d2c8389f74
commit 509f2be3a8
11 changed files with 433 additions and 270 deletions

View file

@ -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)

View file

@ -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]);

View file

@ -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: (
<Stack>
Refresh M3U or filter out groups to pull in streams.
<Button
size="xs"
variant="default"
onClick={() => {
API.refreshPlaylist(parsedEvent.data.account);
setRefreshProgress(parsedEvent.data.account, 0);
}}
>
Refresh Now
</Button>
</Stack>
),
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: (
<Stack>
Refresh M3U or filter out groups to pull in streams.
<Button
size="xs"
variant="default"
onClick={() => {
API.refreshPlaylist(event.data.account);
setRefreshProgress(event.data.account, 0);
}}
>
Refresh Now
</Button>
</Stack>
),
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 (
<WebsocketContext.Provider value={ret}>
{connectionError && !isReady && reconnectAttempts >= maxReconnectAttempts && (
<Alert color="red" title="WebSocket Connection Failed" style={{ position: 'fixed', bottom: 10, right: 10, zIndex: 1000, maxWidth: 350 }}>
{connectionError}
<Button size="xs" mt={10} onClick={() => {
setReconnectAttempts(0);
connectWebSocket();
}}>
Try Again
</Button>
</Alert>
)}
{connectionError && !isReady && reconnectAttempts < maxReconnectAttempts && reconnectAttempts > 0 && (
<Alert color="orange" title="WebSocket Reconnecting" style={{ position: 'fixed', bottom: 10, right: 10, zIndex: 1000, maxWidth: 350 }}>
{connectionError}
</Alert>
)}
{children}
</WebsocketContext.Provider>
);

View file

@ -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 && (
<img

View file

@ -37,7 +37,7 @@ import useEPGsStore from '../../store/epgs';
import { Dropzone } from '@mantine/dropzone';
import { FixedSizeList as List } from 'react-window';
const Channel = ({ channel = null, isOpen, onClose }) => {
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"
/>
<TextInput
id="tvc_guide_stationid"
name="tvc_guide_stationid"
@ -839,4 +839,4 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
);
};
export default Channel;
export default ChannelForm;

View file

@ -134,7 +134,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => {
<Button
type="submit"
variant="contained"
disabled={form.submiting}
disabled={form.submitting} // Fix the typo: submiting -> submitting
size="small"
>
Submit

View file

@ -31,16 +31,6 @@ const LoginForm = () => {
navigate('/channels'); // Or any other route you'd like
};
// // Handle form submission
// const handleSubmit = async (e) => {
// e.preventDefault();
// setLoading(true);
// setError(''); // Reset error on each new submission
// await login(username, password)
// navigate('/channels'); // Or any other route you'd like
// };
return (
<Center
style={{
@ -73,8 +63,8 @@ const LoginForm = () => {
required
/>
<Button type="submit" size="sm" sx={{ pt: 1 }}>
Submit
<Button type="submit" mt="sm">
Login
</Button>
</Stack>
</form>

View file

@ -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.');
}
};

View file

@ -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}`,

View file

@ -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

View file

@ -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 });