diff --git a/client-v3/src/App.vue b/client-v3/src/App.vue
index 77a78467..5b48ac76 100644
--- a/client-v3/src/App.vue
+++ b/client-v3/src/App.vue
@@ -111,9 +111,7 @@
{{ userStore.currentUser.username }}
Settings
-
- Sign Out
-
+ Sign Out
Connected
@@ -401,6 +399,10 @@ async function switchServer(): Promise {
window.location.reload();
}
+async function handleLogout(): Promise {
+ await userStore.logout();
+}
+
async function goToLivePage(): Promise {
const valid = await v$.value.$validate();
if (!valid) return;
diff --git a/client-v3/src/components/show/config/script/ScriptEditor.vue b/client-v3/src/components/show/config/script/ScriptEditor.vue
index b770119e..6e86ec12 100644
--- a/client-v3/src/components/show/config/script/ScriptEditor.vue
+++ b/client-v3/src/components/show/config/script/ScriptEditor.vue
@@ -121,14 +121,8 @@
-
+ Add Dialogue
+
Add Stage Direction
diff --git a/client-v3/src/components/show/config/script/ScriptLineViewer.vue b/client-v3/src/components/show/config/script/ScriptLineViewer.vue
index b8e0cfec..841d6f08 100644
--- a/client-v3/src/components/show/config/script/ScriptLineViewer.vue
+++ b/client-v3/src/components/show/config/script/ScriptLineViewer.vue
@@ -121,26 +121,26 @@
End
-
- Insert Dialogue
- Insert Stage Direction
+ Edit
- Insert Cue Line
- Insert Spacing
- Delete
-
+
+ Insert Dialogue
+ Insert Stage Direction
+ Insert Cue Line
+ Insert Spacing
+ Delete
+
+
diff --git a/client-v3/src/composables/useWebSocket.ts b/client-v3/src/composables/useWebSocket.ts
index ec5d1535..f41f0787 100644
--- a/client-v3/src/composables/useWebSocket.ts
+++ b/client-v3/src/composables/useWebSocket.ts
@@ -169,20 +169,24 @@ function connect(): void {
ws = new WebSocket(wsURL);
ws.onopen = async () => {
- const wasErrored = errorCount > 0;
- wsStore.$patch({ isConnected: true });
- if (wasErrored) {
- toast.success(
- `WebSocket reconnected after ${errorCount} attempt${errorCount > 1 ? 's' : ''}`
- );
- }
- log.info('WebSocket connected');
- if (wasErrored) {
- const { useShowStore } = await import('@/stores/show');
- const showStore = useShowStore();
- if (showStore.currentSession != null) {
- await showStore.getShowSessionData();
+ try {
+ const wasErrored = errorCount > 0;
+ wsStore.$patch({ isConnected: true });
+ if (wasErrored) {
+ toast.success(
+ `WebSocket reconnected after ${errorCount} attempt${errorCount > 1 ? 's' : ''}`
+ );
}
+ log.info('WebSocket connected');
+ if (wasErrored) {
+ const { useShowStore } = await import('@/stores/show');
+ const showStore = useShowStore();
+ if (showStore.currentSession != null) {
+ await showStore.getShowSessionData();
+ }
+ }
+ } catch (e) {
+ log.error('Error in WebSocket onopen handler:', e);
}
};
diff --git a/client-v3/src/js/http-interceptor.ts b/client-v3/src/js/http-interceptor.ts
index f7b2f72b..933c4ac2 100644
--- a/client-v3/src/js/http-interceptor.ts
+++ b/client-v3/src/js/http-interceptor.ts
@@ -19,9 +19,29 @@ function buildAuthenticatedOptions(
return { ...options, headers };
}
+type QueueEntry = {
+ resolve: (r: Response) => void;
+ resource: string;
+ options: RequestInit & { headers: Record };
+};
+
export default function setupHttpInterceptor(): void {
const originalFetch = window.fetch;
- const refreshState = { isRefreshing: false };
+ const refreshState = { isRefreshing: false, queue: [] as QueueEntry[] };
+
+ function flushQueue(newToken: string | null): void {
+ const entries = refreshState.queue.splice(0);
+ entries.forEach(({ resolve, resource, options }) => {
+ if (newToken) {
+ originalFetch(resource, {
+ ...options,
+ headers: { ...options.headers, Authorization: `Bearer ${newToken}` },
+ }).then(resolve);
+ } else {
+ resolve(new Response(JSON.stringify({ message: 'Session expired' }), { status: 401 }));
+ }
+ });
+ }
async function handle401Response(
resource: string,
@@ -30,16 +50,24 @@ export default function setupHttpInterceptor(): void {
isRefreshRequest: boolean,
response: Response
): Promise {
- if (isRefreshRequest || refreshState.isRefreshing) {
- log.warn('Token refresh failed with 401 or already refreshing, logging out');
+ if (isRefreshRequest) {
+ log.warn('Token refresh request received 401, logging out');
+ flushQueue(null);
toast.warning('Your session has expired. Please log in again.');
await userStore.logout();
return response;
}
+ if (refreshState.isRefreshing) {
+ return new Promise((resolve) => {
+ refreshState.queue.push({ resolve, resource, options: newOptions });
+ });
+ }
+
log.info('Attempting token refresh');
if (!userStore.authToken) {
log.warn('401 received with no token present');
+ flushQueue(null);
await userStore.logout();
return response;
}
@@ -51,18 +79,21 @@ export default function setupHttpInterceptor(): void {
if (!refreshSuccess) {
log.warn('Token refresh failed, logging out');
+ flushQueue(null);
toast.warning('Your session has expired. Please log in again.');
await userStore.logout();
return response;
}
log.info('Token refresh successful, retrying original request');
+ flushQueue(userStore.authToken);
return await originalFetch(resource, {
...newOptions,
headers: { ...newOptions.headers, Authorization: `Bearer ${userStore.authToken}` },
});
} catch (refreshError) {
refreshState.isRefreshing = false;
+ flushQueue(null);
log.error('Error during token refresh:', refreshError);
toast.error('Authentication error - please log in again');
await userStore.logout();
diff --git a/client-v3/src/stores/show.ts b/client-v3/src/stores/show.ts
index 39c2ea65..359a6ea2 100644
--- a/client-v3/src/stores/show.ts
+++ b/client-v3/src/stores/show.ts
@@ -753,7 +753,14 @@ export const useShowStore = defineStore('show', {
await this.getScriptRevisions();
toast.success('Added new script revision!');
} else {
- toast.error('Unable to add new script revision');
+ let message = 'Unable to add new script revision';
+ try {
+ const data = await response.json();
+ if (data.message) message = data.message;
+ } catch {
+ /* non-JSON body */
+ }
+ toast.error(message);
}
},
@@ -767,7 +774,14 @@ export const useShowStore = defineStore('show', {
await this.getScriptRevisions();
toast.success('Deleted script revision!');
} else {
- toast.error('Unable to delete script revision');
+ let message = 'Unable to delete script revision';
+ try {
+ const data = await response.json();
+ if (data.message) message = data.message;
+ } catch {
+ /* non-JSON body */
+ }
+ toast.error(message);
}
},
@@ -781,7 +795,14 @@ export const useShowStore = defineStore('show', {
await this.getScriptRevisions();
toast.success('Loaded script revision!');
} else {
- toast.error('Unable to load script revision');
+ let message = 'Unable to load script revision';
+ try {
+ const data = await response.json();
+ if (data.message) message = data.message;
+ } catch {
+ /* non-JSON body */
+ }
+ toast.error(message);
}
},