refactor(admin): move WebSocket from TypeScript to template with server-side token

- Move WebSocket connection logic from admin.ts to admin.templ template
- Inject JWT token directly into WebSocket URL from server-side User.Token
- Remove createWebSocket import and related functions from admin.ts
- Embed complete WebSocket message handling in template script tag

Changes to admin.templ:
- Add inline <script> with WebSocket connection using server-injected token
- Implement scan_progress, scan_complete, and scan_error message handlers
- Include error handling for WebSocket failures

Changes to admin.ts:
- Remove createWebSocket import
- Remove connectWebSocket(), updateScanProgress(), showScanComplete(), showScanError() functions
- Keep stopScanStatusPolling() for Alpine.js integration
- Preserve all other admin functionality (scan triggering, stats loading, etc.)

This refactoring eliminates client-side localStorage dependencies for
WebSocket authentication, making the admin 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.
This commit is contained in:
2026-03-12 15:43:27 -04:00
parent 9bceef7b41
commit 40c447bd19
2 changed files with 59 additions and 48 deletions
+57 -1
View File
@@ -9,6 +9,62 @@ templ Admin(user User) {
<script src="/static/htmx.min.js"></script>
<script src="/static/main.js" defer></script>
<link href="/static/style.css" rel="stylesheet"/>
<script>
let scanWs = null;
function connectScanWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/sync?token={ user.Token }`;
scanWs = new WebSocket(wsUrl);
scanWs.onmessage = function(event) {
try {
const message = JSON.parse(event.data);
switch (message.type) {
case 'scan_progress':
updateScanProgress(message.data);
break;
case 'scan_complete':
showScanComplete(message.data);
stopScanStatusPolling();
break;
case 'scan_error':
showScanError(message.data);
break;
}
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
scanWs.onerror = function(error) {
console.error('WebSocket error:', error);
};
}
function updateScanProgress(data) {
const progressBar = document.getElementById('scan-progress-bar');
if (progressBar) {
progressBar.style.width = (data.progress * 100) + '%';
}
const progressText = document.getElementById('scan-progress-text');
if (progressText) {
progressText.textContent = `${data.files_scanned} files scanned (${data.new_items} new)`;
}
}
function showScanComplete(data) {
console.log('Scan complete:', data);
}
function showScanError(data) {
console.error('Scan error:', data);
}
document.addEventListener('DOMContentLoaded', function() {
connectScanWebSocket();
});
</script>
</head>
<body class="theme-tokyo-night" x-data="admin">
@Header(user, "/admin")
@@ -97,7 +153,7 @@ templ Admin(user User) {
</div>
<div class="mt-4 flex gap-2">
<button
@click="$store.window.location.reload()"
@click="window.location.reload()"
class="btn-primary px-4 py-2 rounded-lg"
>
Refresh to View Books