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:
2026-03-12 15:43:39 -04:00
parent 40c447bd19
commit 93710a1e96
2 changed files with 83 additions and 472 deletions
+81 -8
View File
@@ -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>