Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions client-v3/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@
</BNavbar>

<template v-if="!loaded">
<div class="text-center center-spinner">
<div v-if="startupError" class="text-center center-spinner">
<p class="text-danger">Failed to connect to the server. Please check your connection.</p>
<BButton variant="primary" @click="retryStartup">Retry</BButton>
</div>
<div v-else class="text-center center-spinner">
<BSpinner style="width: 10rem; height: 10rem" variant="info" />
</div>
</template>
Expand Down Expand Up @@ -219,6 +223,7 @@ const isElectronEnv = ref(false);

// Local state
const loaded = ref(false);
const startupError = ref(false);
const stoppingSession = ref(false);
const startingSession = ref(false);
const changingPage = ref(false);
Expand Down Expand Up @@ -253,14 +258,19 @@ onMounted(async () => {
}
}

if (userStore.authToken) {
await userStore.refreshToken();
await userStore.setupTokenRefresh();
}
try {
if (userStore.authToken) {
await userStore.refreshToken();
await userStore.setupTokenRefresh();
}

await systemStore.getSettings();
connect();
await awaitWSConnect();
await systemStore.getSettings();
connect();
await awaitWSConnect();
} catch (e) {
log.error('Startup error:', e);
startupError.value = true;
}
});

onBeforeUnmount(() => {
Expand Down Expand Up @@ -307,6 +317,22 @@ async function awaitWSConnect(): Promise<void> {
}
}

async function retryStartup(): Promise<void> {
startupError.value = false;
try {
if (userStore.authToken) {
await userStore.refreshToken();
await userStore.setupTokenRefresh();
}
await systemStore.getSettings();
connect();
await awaitWSConnect();
} catch (e) {
log.error('Retry startup error:', e);
startupError.value = true;
}
}

async function stopShowSession(): Promise<void> {
stoppingSession.value = true;
const confirmed = await confirm('Are you sure you want to stop the show?', {
Expand Down
7 changes: 6 additions & 1 deletion client-v3/src/components/show/config/script/ScriptEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,12 @@ async function saveScript(): Promise<void> {

savingInProgress.value = true;
saveError.value = false;
await scriptStore.getMaxPage();
const maxPageOk = await scriptStore.getMaxPage();
if (!maxPageOk) {
toast.error('Unable to save script — could not determine page count. Please try again.');
savingInProgress.value = false;
return;
}
const tmpPageKeys = Object.keys(scriptConfigStore.tmpScript).map((x) => Number.parseInt(x, 10));
const maxPage = Math.max(scriptStore.maxPage, ...tmpPageKeys, 0);
totalSavePages.value = maxPage;
Expand Down
5 changes: 3 additions & 2 deletions client-v3/src/composables/useWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ async function handleMessage(msg: WsMessage): Promise<void> {
case 'WS_AUTH_ERROR':
wsStore.$patch({ authenticated: false, pendingAuthentication: false });
log.error('WebSocket authentication error:', msg.DATA);
toast.error('WebSocket authentication failed. Please log in again.');
await userStore.logout();
break;
case 'WS_TOKEN_REFRESH_SUCCESS':
log.info('WebSocket token refreshed successfully');
Expand Down Expand Up @@ -108,8 +110,7 @@ function screamingToCamel(s: string): string {
async function dispatchAction(action: string, data: Record<string, unknown>): Promise<void> {
// Actions that can't be auto-routed by naming convention
if (action === 'TOKEN_REFRESH') {
const payload = data as { DATA: { access_token: string } };
await useUserStore().tokenRefreshFromServer(payload.DATA.access_token);
await useUserStore().tokenRefreshFromServer((data as { access_token: string }).access_token);
return;
}
if (action === 'SHOW_CHANGED') {
Expand Down
5 changes: 4 additions & 1 deletion client-v3/src/stores/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,15 @@ export const useScriptStore = defineStore('script', {
return response.ok;
},

async getMaxPage(): Promise<void> {
async getMaxPage(): Promise<boolean> {
const response = await fetch(makeURL('/api/v1/show/script/max_page'));
if (response.ok) {
const data = await response.json();
this.maxPage = data.max_page;
return true;
}
log.error('Unable to fetch max page');
return false;
},

async getStageDirectionStyles(): Promise<void> {
Expand Down
52 changes: 32 additions & 20 deletions client-v3/src/stores/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,19 @@ export const useUserStore = defineStore('user', {
const data = await response.json();
if (data.access_token) this._setToken(data.access_token);

const { useSystemStore } = await import('@/stores/system');
await useSystemStore().getRbacRoles();
await this.getCurrentUser();
await this.getCurrentRbac();
await this.getUserSettings();
await this.setupTokenRefresh();
try {
const { useSystemStore } = await import('@/stores/system');
await useSystemStore().getRbacRoles();
await this.getCurrentUser();
await this.getCurrentRbac();
await this.getUserSettings();
await this.setupTokenRefresh();
} catch (e) {
log.error('Error loading user data after login:', e);
this._clearToken();
toast.error('Login failed — unable to load user data. Please try again.');
return false;
}

// Trigger WS authentication if the connection is waiting
wsStore.triggerAuthentication();
Expand Down Expand Up @@ -119,21 +126,26 @@ export const useUserStore = defineStore('user', {

async refreshToken(): Promise<boolean> {
if (!this.authToken) return false;
const response = await fetch(makeURL('/api/v1/auth/refresh-token'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (response.ok) {
const data = await response.json();
this._setToken(data.access_token);
const { useWebSocketStore } = await import('@/stores/websocket');
useWebSocketStore().refreshWsToken();
log.debug('Token refreshed successfully');
return true;
try {
const response = await fetch(makeURL('/api/v1/auth/refresh-token'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (response.ok) {
const data = await response.json();
this._setToken(data.access_token);
const { useWebSocketStore } = await import('@/stores/websocket');
useWebSocketStore().refreshWsToken();
log.debug('Token refreshed successfully');
return true;
}
log.error('Failed to refresh token');
return false;
} catch (e) {
log.error('Network error during token refresh:', e);
return false;
}
log.error('Failed to refresh token');
return false;
},

async tokenRefreshFromServer(newToken: string): Promise<void> {
Expand Down
Loading