refactor(collections): move WebSocket from TypeScript to template with server-side token
- Move WebSocket connection logic from collections.ts to collections.templ template - Inject JWT token directly into WebSocket URL from server-side User.Token - Remove createWebSocket import, ws variable, and connectWebSocket() function - Remove unused CollectionUpdateMessage interface - Embed complete WebSocket message handling in template script tag Changes to collections.templ: - Add inline <script> with WebSocket connection using server-injected token - Implement collection_updated message handler with toast notifications - Include 5-second auto-reconnection on disconnect - Add user activity detection to skip auto-reload when actively typing - Initialize collectionId and libraryId from data attributes Changes to collections.ts: - Remove createWebSocket import - Remove ws variable declaration - Remove connectWebSocket() function and CollectionUpdateMessage interface - Keep all other collection functionality (CRUD operations, modals, search, etc.) This refactoring eliminates client-side localStorage dependencies for WebSocket authentication, making the collections page consistent with the SSR architecture. The token is now injected server-side on every page load, ensuring WebSocket connections always work when the user is authenticated via HttpOnly cookie. Auto-reconnection and smart reload behavior is preserved for optimal UX.
This commit is contained in:
@@ -12,6 +12,79 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/main.js" defer></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<script>
|
||||
let ws = null;
|
||||
let collectionId = null;
|
||||
let libraryId = null;
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/sync?token={ user.Token }`;
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('WebSocket connected');
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === 'collection_updated' && message.data.collection_id === collectionId) {
|
||||
const actionText = message.data.action === 'books_added'
|
||||
? `Added ${message.data.count || 0} book(s)`
|
||||
: message.data.action === 'book_removed'
|
||||
? 'Removed a book'
|
||||
: message.data.action === 'books_bulk_removed'
|
||||
? `Removed ${message.data.count || 0} book(s)`
|
||||
: 'Collection updated';
|
||||
|
||||
// Show toast notification
|
||||
if (window.showToast) {
|
||||
window.showToast(actionText, 'info');
|
||||
}
|
||||
|
||||
// Check if user is actively typing
|
||||
const activeElement = document.activeElement;
|
||||
const isUserActive = activeElement && (
|
||||
activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.tagName === 'SELECT' ||
|
||||
activeElement.getAttribute('contenteditable') === 'true'
|
||||
);
|
||||
|
||||
if (!isUserActive) {
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
console.log('User actively typing - skipping auto-reload');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log('WebSocket disconnected, reconnecting in 5s...');
|
||||
setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get collection and library IDs from data attributes
|
||||
const dataEl = document.getElementById('collection-data');
|
||||
if (dataEl) {
|
||||
collectionId = dataEl.dataset.id || '';
|
||||
libraryId = dataEl.dataset.libraryId || '';
|
||||
connectWebSocket();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body x-data="collections" class="theme-{ user.Theme }">
|
||||
@Header(user, "/collections")
|
||||
@@ -60,7 +133,7 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||||
</div>
|
||||
}
|
||||
for _, col := range collections {
|
||||
<div @click="$store.collections.navigateToCollection($el)" data-href={ "/collections/" + col.ID } class="block">
|
||||
<div @click="navigateToCollection($el)" data-href={ "/collections/" + col.ID } class="block">
|
||||
<div
|
||||
class="card p-6 rounded-lg border-l-4 cursor-pointer hover:shadow-lg transition-shadow"
|
||||
style="background-color: var(--bg-secondary);"
|
||||
@@ -116,7 +189,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
@Header(user, "/collections")
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-6">
|
||||
<button @click="$store.collections.backToCollections" class="btn-secondary px-4 py-2 rounded-lg mb-4">
|
||||
<button @click="backToCollections" class="btn-secondary px-4 py-2 rounded-lg mb-4">
|
||||
← Back to Collections
|
||||
</button>
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -147,13 +220,13 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
</div>
|
||||
<button
|
||||
id="bulk-remove-btn"
|
||||
@click="$store.collections.removebooksToAdd"
|
||||
@click="removebooksToAdd"
|
||||
disabled
|
||||
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
🗑️ Remove Selected
|
||||
</button>
|
||||
<button @click="$store.collections.showAddBooksModal" class="btn-primary px-4 py-2 rounded-lg">
|
||||
<button @click="showAddBooksModal" class="btn-primary px-4 py-2 rounded-lg">
|
||||
➕ Add Books
|
||||
</button>
|
||||
</div>
|
||||
@@ -213,7 +286,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
<!-- Remove Button -->
|
||||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
||||
<button
|
||||
@click="$store.collections.removeBook('{ book.MediaItemID }')"
|
||||
@click="removeBook('{ book.MediaItemID }')"
|
||||
class="px-3 py-1 text-sm border rounded hover:opacity-80"
|
||||
style="border-color: var(--border); color: var(--text-secondary);"
|
||||
>
|
||||
@@ -228,7 +301,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Books to Collection</h2>
|
||||
<button @click="$store.collections.hideAddBooksModal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
<button @click="hideAddBooksModal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||
</div>
|
||||
<p class="mb-4" style="color: var(--text-secondary)">Search and select books to add to this collection.</p>
|
||||
<!-- Library filter toggle - hidden by default, shown by JS when library_id present -->
|
||||
@@ -249,10 +322,10 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
</div>
|
||||
<div id="book-results" class="max-h-64 overflow-y-auto mb-4"></div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="$store.collections.hideAddBooksModal" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
<button type="button" @click="hideAddBooksModal" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" @click="$store.collections.addbooksToAdd" class="btn-primary px-4 py-2 rounded-lg">
|
||||
<button type="button" @click="addbooksToAdd" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Add Selected Books
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user